@ainyc/canonry 1.41.0 → 1.44.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/assets/index--ev1Bjls.css +1 -0
- package/assets/assets/index-BAzKj_9S.js +281 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-EUBC5EGC.js → chunk-AATIMNOX.js} +35 -1
- package/dist/{chunk-TKMBOLZB.js → chunk-C3LF36DQ.js} +441 -49
- package/dist/cli.js +287 -11
- package/dist/index.js +2 -2
- package/dist/{intelligence-service-DXGTIRF5.js → intelligence-service-36ERONKI.js} +1 -1
- package/package.json +7 -7
- package/assets/assets/index-Djm1st6N.css +0 -1
- package/assets/assets/index-iy29Cmx8.js +0 -281
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
competitors,
|
|
7
7
|
createLogger,
|
|
8
8
|
gaAiReferrals,
|
|
9
|
+
gaSocialReferrals,
|
|
9
10
|
gaTrafficSnapshots,
|
|
10
11
|
gaTrafficSummaries,
|
|
11
12
|
gscCoverageSnapshots,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
runs,
|
|
22
23
|
schedules,
|
|
23
24
|
usageCounters
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AATIMNOX.js";
|
|
25
26
|
|
|
26
27
|
// src/config.ts
|
|
27
28
|
import fs from "fs";
|
|
@@ -1101,6 +1102,14 @@ var ga4AiReferralDtoSchema = z12.object({
|
|
|
1101
1102
|
users: z12.number(),
|
|
1102
1103
|
sourceDimension: ga4SourceDimensionSchema
|
|
1103
1104
|
});
|
|
1105
|
+
var ga4SocialReferralDtoSchema = z12.object({
|
|
1106
|
+
source: z12.string(),
|
|
1107
|
+
medium: z12.string(),
|
|
1108
|
+
sessions: z12.number(),
|
|
1109
|
+
users: z12.number(),
|
|
1110
|
+
/** GA4 default channel group (e.g. 'Organic Social', 'Paid Social') */
|
|
1111
|
+
channelGroup: z12.string()
|
|
1112
|
+
});
|
|
1104
1113
|
var ga4TrafficSummaryDtoSchema = z12.object({
|
|
1105
1114
|
totalSessions: z12.number(),
|
|
1106
1115
|
totalOrganicSessions: z12.number(),
|
|
@@ -1116,6 +1125,17 @@ var ga4TrafficSummaryDtoSchema = z12.object({
|
|
|
1116
1125
|
aiSessionsDeduped: z12.number(),
|
|
1117
1126
|
/** Deduped AI user total: MAX(users) per date+source+medium across attribution dimensions, then summed. */
|
|
1118
1127
|
aiUsersDeduped: z12.number(),
|
|
1128
|
+
socialReferrals: z12.array(ga4SocialReferralDtoSchema),
|
|
1129
|
+
/** Total social sessions (session-scoped, no cross-dimension dedup needed). */
|
|
1130
|
+
socialSessions: z12.number(),
|
|
1131
|
+
/** Total social users (session-scoped, no cross-dimension dedup needed). */
|
|
1132
|
+
socialUsers: z12.number(),
|
|
1133
|
+
/** Organic sessions as a percentage of total sessions (0–100, rounded). */
|
|
1134
|
+
organicSharePct: z12.number(),
|
|
1135
|
+
/** Deduped AI sessions as a percentage of total sessions (0–100, rounded). */
|
|
1136
|
+
aiSharePct: z12.number(),
|
|
1137
|
+
/** Social sessions as a percentage of total sessions (0–100, rounded). */
|
|
1138
|
+
socialSharePct: z12.number(),
|
|
1119
1139
|
lastSyncedAt: z12.string().nullable()
|
|
1120
1140
|
});
|
|
1121
1141
|
var ga4AiReferralHistoryEntrySchema = z12.object({
|
|
@@ -1127,6 +1147,15 @@ var ga4AiReferralHistoryEntrySchema = z12.object({
|
|
|
1127
1147
|
/** Which GA4 dimension this row came from: session (sessionSource), first_user (firstUserSource), or manual_utm (utm_source parameter) */
|
|
1128
1148
|
sourceDimension: ga4SourceDimensionSchema
|
|
1129
1149
|
});
|
|
1150
|
+
var ga4SocialReferralHistoryEntrySchema = z12.object({
|
|
1151
|
+
date: z12.string(),
|
|
1152
|
+
source: z12.string(),
|
|
1153
|
+
medium: z12.string(),
|
|
1154
|
+
sessions: z12.number(),
|
|
1155
|
+
users: z12.number(),
|
|
1156
|
+
/** GA4 default channel group (e.g. 'Organic Social', 'Paid Social') */
|
|
1157
|
+
channelGroup: z12.string()
|
|
1158
|
+
});
|
|
1130
1159
|
var ga4SessionHistoryEntrySchema = z12.object({
|
|
1131
1160
|
date: z12.string(),
|
|
1132
1161
|
sessions: z12.number(),
|
|
@@ -5296,6 +5325,42 @@ var routeCatalog = [
|
|
|
5296
5325
|
404: { description: "Project not found." }
|
|
5297
5326
|
}
|
|
5298
5327
|
},
|
|
5328
|
+
{
|
|
5329
|
+
method: "get",
|
|
5330
|
+
path: "/api/v1/projects/{name}/ga/social-referral-history",
|
|
5331
|
+
summary: "Get social media referral sessions per day grouped by source",
|
|
5332
|
+
tags: ["ga4"],
|
|
5333
|
+
parameters: [nameParameter],
|
|
5334
|
+
responses: {
|
|
5335
|
+
200: { description: "Social referral history returned." },
|
|
5336
|
+
400: { description: "GA4 is not connected." },
|
|
5337
|
+
404: { description: "Project not found." }
|
|
5338
|
+
}
|
|
5339
|
+
},
|
|
5340
|
+
{
|
|
5341
|
+
method: "get",
|
|
5342
|
+
path: "/api/v1/projects/{name}/ga/social-referral-trend",
|
|
5343
|
+
summary: "Get social referral trend (7d/30d) with biggest mover",
|
|
5344
|
+
tags: ["ga4"],
|
|
5345
|
+
parameters: [nameParameter],
|
|
5346
|
+
responses: {
|
|
5347
|
+
200: { description: "Social referral trend returned." },
|
|
5348
|
+
400: { description: "GA4 is not connected." },
|
|
5349
|
+
404: { description: "Project not found." }
|
|
5350
|
+
}
|
|
5351
|
+
},
|
|
5352
|
+
{
|
|
5353
|
+
method: "get",
|
|
5354
|
+
path: "/api/v1/projects/{name}/ga/attribution-trend",
|
|
5355
|
+
summary: "Get per-channel attribution trends (7d/30d) for organic, AI, and social",
|
|
5356
|
+
tags: ["ga4"],
|
|
5357
|
+
parameters: [nameParameter],
|
|
5358
|
+
responses: {
|
|
5359
|
+
200: { description: "Attribution trend returned." },
|
|
5360
|
+
400: { description: "GA4 is not connected." },
|
|
5361
|
+
404: { description: "Project not found." }
|
|
5362
|
+
}
|
|
5363
|
+
},
|
|
5299
5364
|
{
|
|
5300
5365
|
method: "get",
|
|
5301
5366
|
path: "/api/v1/projects/{name}/ga/session-history",
|
|
@@ -5891,6 +5956,7 @@ var GSC_DATA_LAG_DAYS = 3;
|
|
|
5891
5956
|
var INDEXING_API_BASE = "https://indexing.googleapis.com/v3";
|
|
5892
5957
|
var INDEXING_API_DAILY_LIMIT = 200;
|
|
5893
5958
|
var GOOGLE_REQUEST_TIMEOUT_MS = 3e4;
|
|
5959
|
+
var GSC_MAX_PAGES = 40;
|
|
5894
5960
|
|
|
5895
5961
|
// ../integration-google/src/types.ts
|
|
5896
5962
|
var GoogleAuthError = class extends Error {
|
|
@@ -5989,7 +6055,15 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
|
|
|
5989
6055
|
});
|
|
5990
6056
|
if (!res.ok) {
|
|
5991
6057
|
const body = await res.text();
|
|
5992
|
-
|
|
6058
|
+
let detail = "";
|
|
6059
|
+
try {
|
|
6060
|
+
const parsed = JSON.parse(body);
|
|
6061
|
+
if (parsed.error) detail = parsed.error;
|
|
6062
|
+
if (parsed.error_description) detail += detail ? `: ${parsed.error_description}` : parsed.error_description;
|
|
6063
|
+
} catch {
|
|
6064
|
+
detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
|
|
6065
|
+
}
|
|
6066
|
+
throw new GoogleAuthError(`Token exchange failed (${res.status}): ${detail}`);
|
|
5993
6067
|
}
|
|
5994
6068
|
return await res.json();
|
|
5995
6069
|
}
|
|
@@ -6010,7 +6084,15 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
|
|
|
6010
6084
|
});
|
|
6011
6085
|
if (!res.ok) {
|
|
6012
6086
|
const body = await res.text();
|
|
6013
|
-
|
|
6087
|
+
let detail = "";
|
|
6088
|
+
try {
|
|
6089
|
+
const parsed = JSON.parse(body);
|
|
6090
|
+
if (parsed.error) detail = parsed.error;
|
|
6091
|
+
if (parsed.error_description) detail += detail ? `: ${parsed.error_description}` : parsed.error_description;
|
|
6092
|
+
} catch {
|
|
6093
|
+
detail = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
|
|
6094
|
+
}
|
|
6095
|
+
throw new GoogleAuthError(`Token refresh failed (${res.status}): ${detail}`);
|
|
6014
6096
|
}
|
|
6015
6097
|
return await res.json();
|
|
6016
6098
|
}
|
|
@@ -6057,6 +6139,18 @@ function validateUrl(urlParam) {
|
|
|
6057
6139
|
throw new GoogleApiError("URL must be a valid URL", 400);
|
|
6058
6140
|
}
|
|
6059
6141
|
}
|
|
6142
|
+
function validateDate(date, label) {
|
|
6143
|
+
if (!date || typeof date !== "string" || date.trim().length === 0) {
|
|
6144
|
+
throw new GoogleApiError(`${label} is required and must be a non-empty string`, 400);
|
|
6145
|
+
}
|
|
6146
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
6147
|
+
throw new GoogleApiError(`${label} must be in YYYY-MM-DD format, got "${date}"`, 400);
|
|
6148
|
+
}
|
|
6149
|
+
const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
6150
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
6151
|
+
throw new GoogleApiError(`${label} is not a valid date, got "${date}"`, 400);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6060
6154
|
function gscClientLog(level, action, ctx) {
|
|
6061
6155
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GscClient", action, ...ctx };
|
|
6062
6156
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -6085,7 +6179,8 @@ async function gscFetch(accessToken, url, opts) {
|
|
|
6085
6179
|
if (!res.ok) {
|
|
6086
6180
|
const body = await res.text();
|
|
6087
6181
|
gscClientLog("error", "http.error", { url, method, httpStatus: res.status });
|
|
6088
|
-
|
|
6182
|
+
const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
6183
|
+
throw new GoogleApiError(`GSC API error (${res.status}): ${detail}`, res.status);
|
|
6089
6184
|
}
|
|
6090
6185
|
return await res.json();
|
|
6091
6186
|
}
|
|
@@ -6110,10 +6205,16 @@ async function listSitemaps(accessToken, siteUrl) {
|
|
|
6110
6205
|
async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
6111
6206
|
validateAccessToken(accessToken);
|
|
6112
6207
|
validateSiteUrl(siteUrl);
|
|
6208
|
+
validateDate(opts.startDate, "startDate");
|
|
6209
|
+
validateDate(opts.endDate, "endDate");
|
|
6113
6210
|
const allRows = [];
|
|
6114
6211
|
let startRow = 0;
|
|
6115
6212
|
const dimensions = opts.dimensions ?? ["query", "page", "country", "device", "date"];
|
|
6116
6213
|
for (; ; ) {
|
|
6214
|
+
if (startRow >= GSC_MAX_ROWS_PER_REQUEST * GSC_MAX_PAGES) {
|
|
6215
|
+
gscClientLog("error", "pagination.safety-limit", { siteUrl, startRow, maxRows: GSC_MAX_ROWS_PER_REQUEST * GSC_MAX_PAGES });
|
|
6216
|
+
break;
|
|
6217
|
+
}
|
|
6117
6218
|
const requestBody = {
|
|
6118
6219
|
startDate: opts.startDate,
|
|
6119
6220
|
endDate: opts.endDate,
|
|
@@ -6183,6 +6284,7 @@ var GOOGLE_TOKEN_URL2 = "https://oauth2.googleapis.com/token";
|
|
|
6183
6284
|
var GA4_DEFAULT_SYNC_DAYS = 30;
|
|
6184
6285
|
var GA4_MAX_SYNC_DAYS = 90;
|
|
6185
6286
|
var GA4_REQUEST_TIMEOUT_MS = 3e4;
|
|
6287
|
+
var GA4_MAX_PAGES = 50;
|
|
6186
6288
|
|
|
6187
6289
|
// ../integration-google-analytics/src/types.ts
|
|
6188
6290
|
var GA4ApiError = class extends Error {
|
|
@@ -6268,7 +6370,8 @@ async function getAccessToken(clientEmail, privateKey) {
|
|
|
6268
6370
|
if (!res.ok) {
|
|
6269
6371
|
const body = await res.text().catch(() => "");
|
|
6270
6372
|
ga4Log("error", "token.failed", { httpStatus: res.status });
|
|
6271
|
-
|
|
6373
|
+
const detail = body.length <= 200 ? body : `${body.slice(0, 200)}... [truncated]`;
|
|
6374
|
+
throw new GA4ApiError(`Failed to get access token: ${detail}`, res.status);
|
|
6272
6375
|
}
|
|
6273
6376
|
const data = await res.json();
|
|
6274
6377
|
return data.access_token;
|
|
@@ -6310,7 +6413,8 @@ async function runReport(accessToken, propertyId, request) {
|
|
|
6310
6413
|
if (!res.ok) {
|
|
6311
6414
|
const body = await res.text();
|
|
6312
6415
|
ga4Log("error", "report.error", { propertyId, httpStatus: res.status });
|
|
6313
|
-
|
|
6416
|
+
const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
6417
|
+
throw new GA4ApiError(`GA4 API error (${res.status}): ${detail}`, res.status);
|
|
6314
6418
|
}
|
|
6315
6419
|
return await res.json();
|
|
6316
6420
|
}
|
|
@@ -6340,7 +6444,8 @@ async function batchRunReports(accessToken, propertyId, requests) {
|
|
|
6340
6444
|
if (!res.ok) {
|
|
6341
6445
|
const body = await res.text();
|
|
6342
6446
|
ga4Log("error", "batch-report.error", { propertyId, httpStatus: res.status });
|
|
6343
|
-
|
|
6447
|
+
const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
6448
|
+
throw new GA4ApiError(`GA4 API error (${res.status}): ${detail}`, res.status);
|
|
6344
6449
|
}
|
|
6345
6450
|
const data = await res.json();
|
|
6346
6451
|
return data.reports;
|
|
@@ -6371,7 +6476,9 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6371
6476
|
const PAGE_SIZE = 1e4;
|
|
6372
6477
|
const rows = [];
|
|
6373
6478
|
let offset = 0;
|
|
6374
|
-
|
|
6479
|
+
let pageCount = 0;
|
|
6480
|
+
while (pageCount < GA4_MAX_PAGES) {
|
|
6481
|
+
pageCount++;
|
|
6375
6482
|
const request = {
|
|
6376
6483
|
dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
|
|
6377
6484
|
dimensions: [
|
|
@@ -6401,7 +6508,9 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6401
6508
|
}
|
|
6402
6509
|
const organicMap = /* @__PURE__ */ new Map();
|
|
6403
6510
|
let organicOffset = 0;
|
|
6404
|
-
|
|
6511
|
+
let organicPageCount = 0;
|
|
6512
|
+
while (organicPageCount < GA4_MAX_PAGES) {
|
|
6513
|
+
organicPageCount++;
|
|
6405
6514
|
const organicRequest = {
|
|
6406
6515
|
dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
|
|
6407
6516
|
dimensions: [{ name: "date" }, { name: "landingPagePlusQueryString" }],
|
|
@@ -6515,7 +6624,9 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
6515
6624
|
];
|
|
6516
6625
|
for (const [sourceDim, mediumDim, dimLabel] of dimensionPairs) {
|
|
6517
6626
|
let offset = 0;
|
|
6518
|
-
|
|
6627
|
+
let aiRefPageCount = 0;
|
|
6628
|
+
while (aiRefPageCount < GA4_MAX_PAGES) {
|
|
6629
|
+
aiRefPageCount++;
|
|
6519
6630
|
const request = {
|
|
6520
6631
|
dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
|
|
6521
6632
|
dimensions: [
|
|
@@ -6578,6 +6689,66 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
6578
6689
|
ga4Log("info", "fetch-ai-referrals.done", { propertyId, rowCount: dedupedRows.length });
|
|
6579
6690
|
return dedupedRows;
|
|
6580
6691
|
}
|
|
6692
|
+
var SOCIAL_CHANNEL_GROUPS = ["Organic Social", "Paid Social"];
|
|
6693
|
+
async function fetchSocialReferrals(accessToken, propertyId, days) {
|
|
6694
|
+
validateAccessToken2(accessToken);
|
|
6695
|
+
validatePropertyId(propertyId);
|
|
6696
|
+
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
6697
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
6698
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
6699
|
+
startDate.setDate(startDate.getDate() - syncDays);
|
|
6700
|
+
ga4Log("info", "fetch-social-referrals.start", { propertyId, days: syncDays });
|
|
6701
|
+
const PAGE_SIZE = 1e3;
|
|
6702
|
+
const rows = [];
|
|
6703
|
+
let offset = 0;
|
|
6704
|
+
while (true) {
|
|
6705
|
+
const request = {
|
|
6706
|
+
dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
|
|
6707
|
+
dimensions: [
|
|
6708
|
+
{ name: "date" },
|
|
6709
|
+
{ name: "sessionSource" },
|
|
6710
|
+
{ name: "sessionMedium" },
|
|
6711
|
+
{ name: "sessionDefaultChannelGroup" }
|
|
6712
|
+
],
|
|
6713
|
+
metrics: [
|
|
6714
|
+
{ name: "sessions" },
|
|
6715
|
+
{ name: "totalUsers" }
|
|
6716
|
+
],
|
|
6717
|
+
dimensionFilter: {
|
|
6718
|
+
orGroup: {
|
|
6719
|
+
expressions: SOCIAL_CHANNEL_GROUPS.map((value) => ({
|
|
6720
|
+
filter: {
|
|
6721
|
+
fieldName: "sessionDefaultChannelGroup",
|
|
6722
|
+
stringFilter: { matchType: "EXACT", value }
|
|
6723
|
+
}
|
|
6724
|
+
}))
|
|
6725
|
+
}
|
|
6726
|
+
},
|
|
6727
|
+
limit: PAGE_SIZE,
|
|
6728
|
+
offset
|
|
6729
|
+
};
|
|
6730
|
+
const response = await runReport(accessToken, propertyId, request);
|
|
6731
|
+
const pageRows = (response.rows ?? []).map((row) => ({
|
|
6732
|
+
date: row.dimensionValues[0].value,
|
|
6733
|
+
source: row.dimensionValues[1].value,
|
|
6734
|
+
medium: row.dimensionValues[2].value,
|
|
6735
|
+
sessions: parseInt(row.metricValues[0].value, 10) || 0,
|
|
6736
|
+
users: parseInt(row.metricValues[1].value, 10) || 0,
|
|
6737
|
+
channelGroup: row.dimensionValues[3].value
|
|
6738
|
+
}));
|
|
6739
|
+
rows.push(...pageRows);
|
|
6740
|
+
const totalRows = response.rowCount ?? 0;
|
|
6741
|
+
offset += pageRows.length;
|
|
6742
|
+
if (pageRows.length < PAGE_SIZE || offset >= totalRows) break;
|
|
6743
|
+
}
|
|
6744
|
+
for (const row of rows) {
|
|
6745
|
+
if (row.date.length === 8 && !row.date.includes("-")) {
|
|
6746
|
+
row.date = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
ga4Log("info", "fetch-social-referrals.done", { propertyId, rowCount: rows.length });
|
|
6750
|
+
return rows;
|
|
6751
|
+
}
|
|
6581
6752
|
|
|
6582
6753
|
// ../api-routes/src/google.ts
|
|
6583
6754
|
function signState(payload, secret) {
|
|
@@ -7380,7 +7551,8 @@ async function bingFetch(apiKey, endpoint, opts) {
|
|
|
7380
7551
|
if (!res.ok) {
|
|
7381
7552
|
const body = await res.text();
|
|
7382
7553
|
bingClientLog("error", "http.error", { endpoint, method, httpStatus: res.status });
|
|
7383
|
-
|
|
7554
|
+
const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
|
|
7555
|
+
throw new BingApiError(`Bing API error (${res.status}): ${detail}`, res.status);
|
|
7384
7556
|
}
|
|
7385
7557
|
const text = await res.text();
|
|
7386
7558
|
if (!text || text.trim() === "") {
|
|
@@ -7422,6 +7594,12 @@ async function submitUrlBatch(apiKey, siteUrl, urls) {
|
|
|
7422
7594
|
validateApiKey(apiKey);
|
|
7423
7595
|
validateSiteUrl2(siteUrl);
|
|
7424
7596
|
validateUrls(urls);
|
|
7597
|
+
if (urls.length > BING_SUBMIT_URL_DAILY_LIMIT) {
|
|
7598
|
+
throw new BingApiError(
|
|
7599
|
+
`URL batch exceeds daily limit of ${BING_SUBMIT_URL_DAILY_LIMIT}. Got ${urls.length} URLs.`,
|
|
7600
|
+
400
|
|
7601
|
+
);
|
|
7602
|
+
}
|
|
7425
7603
|
for (let i = 0; i < urls.length; i += BING_SUBMIT_URL_BATCH_LIMIT) {
|
|
7426
7604
|
const batch = urls.slice(i, i + BING_SUBMIT_URL_BATCH_LIMIT);
|
|
7427
7605
|
await bingFetch(apiKey, "SubmitUrlbatch", {
|
|
@@ -8163,6 +8341,7 @@ async function ga4Routes(app, opts) {
|
|
|
8163
8341
|
app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8164
8342
|
app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
|
|
8165
8343
|
app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
|
|
8344
|
+
app.db.delete(gaSocialReferrals).where(eq17(gaSocialReferrals.projectId, project.id)).run();
|
|
8166
8345
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
8167
8346
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
8168
8347
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -8197,17 +8376,37 @@ async function ga4Routes(app, opts) {
|
|
|
8197
8376
|
app.post("/projects/:name/ga/sync", async (request, _reply) => {
|
|
8198
8377
|
const project = resolveProject(app.db, request.params.name);
|
|
8199
8378
|
const days = request.body?.days ?? 30;
|
|
8379
|
+
const only = request.body?.only;
|
|
8380
|
+
const validOnlyValues = ["traffic", "ai", "social"];
|
|
8381
|
+
if (only !== void 0 && !validOnlyValues.includes(only)) {
|
|
8382
|
+
throw validationError(`Invalid "only" value "${only}". Must be one of: ${validOnlyValues.join(", ")}`);
|
|
8383
|
+
}
|
|
8384
|
+
const syncTraffic = !only || only === "traffic";
|
|
8385
|
+
const syncAi = !only || only === "ai";
|
|
8386
|
+
const syncSocial = !only || only === "social";
|
|
8387
|
+
const syncSummary = !only;
|
|
8200
8388
|
const { accessToken, propertyId } = await resolveGa4AccessToken(opts, project.name, project.canonicalDomain);
|
|
8201
|
-
let rows;
|
|
8389
|
+
let rows = [];
|
|
8202
8390
|
let summary;
|
|
8203
|
-
let aiReferrals;
|
|
8391
|
+
let aiReferrals = [];
|
|
8392
|
+
let socialReferrals = [];
|
|
8204
8393
|
try {
|
|
8205
|
-
;
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
]
|
|
8394
|
+
const fetches = [fetchAggregateSummary(accessToken, propertyId, days)];
|
|
8395
|
+
if (syncTraffic) fetches.push(fetchTrafficByLandingPage(accessToken, propertyId, days));
|
|
8396
|
+
if (syncAi) fetches.push(fetchAiReferrals(accessToken, propertyId, days));
|
|
8397
|
+
if (syncSocial) fetches.push(fetchSocialReferrals(accessToken, propertyId, days));
|
|
8398
|
+
const results = await Promise.all(fetches);
|
|
8399
|
+
summary = results[0];
|
|
8400
|
+
let idx = 1;
|
|
8401
|
+
if (syncTraffic) {
|
|
8402
|
+
rows = results[idx++];
|
|
8403
|
+
}
|
|
8404
|
+
if (syncAi) {
|
|
8405
|
+
aiReferrals = results[idx++];
|
|
8406
|
+
}
|
|
8407
|
+
if (syncSocial) {
|
|
8408
|
+
socialReferrals = results[idx++];
|
|
8409
|
+
}
|
|
8211
8410
|
} catch (e) {
|
|
8212
8411
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8213
8412
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, error: msg });
|
|
@@ -8215,14 +8414,14 @@ async function ga4Routes(app, opts) {
|
|
|
8215
8414
|
}
|
|
8216
8415
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8217
8416
|
app.db.transaction((tx) => {
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8417
|
+
if (syncTraffic) {
|
|
8418
|
+
tx.delete(gaTrafficSnapshots).where(
|
|
8419
|
+
and6(
|
|
8420
|
+
eq17(gaTrafficSnapshots.projectId, project.id),
|
|
8421
|
+
sql3`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
8422
|
+
sql3`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
8423
|
+
)
|
|
8424
|
+
).run();
|
|
8226
8425
|
for (const row of rows) {
|
|
8227
8426
|
tx.insert(gaTrafficSnapshots).values({
|
|
8228
8427
|
id: crypto16.randomUUID(),
|
|
@@ -8236,14 +8435,14 @@ async function ga4Routes(app, opts) {
|
|
|
8236
8435
|
}).run();
|
|
8237
8436
|
}
|
|
8238
8437
|
}
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8438
|
+
if (syncAi) {
|
|
8439
|
+
tx.delete(gaAiReferrals).where(
|
|
8440
|
+
and6(
|
|
8441
|
+
eq17(gaAiReferrals.projectId, project.id),
|
|
8442
|
+
sql3`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
8443
|
+
sql3`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
8444
|
+
)
|
|
8445
|
+
).run();
|
|
8247
8446
|
for (const row of aiReferrals) {
|
|
8248
8447
|
tx.insert(gaAiReferrals).values({
|
|
8249
8448
|
id: crypto16.randomUUID(),
|
|
@@ -8258,31 +8457,60 @@ async function ga4Routes(app, opts) {
|
|
|
8258
8457
|
}).run();
|
|
8259
8458
|
}
|
|
8260
8459
|
}
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8460
|
+
if (syncSocial) {
|
|
8461
|
+
tx.delete(gaSocialReferrals).where(
|
|
8462
|
+
and6(
|
|
8463
|
+
eq17(gaSocialReferrals.projectId, project.id),
|
|
8464
|
+
sql3`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
8465
|
+
sql3`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
8466
|
+
)
|
|
8467
|
+
).run();
|
|
8468
|
+
for (const row of socialReferrals) {
|
|
8469
|
+
tx.insert(gaSocialReferrals).values({
|
|
8470
|
+
id: crypto16.randomUUID(),
|
|
8471
|
+
projectId: project.id,
|
|
8472
|
+
date: row.date,
|
|
8473
|
+
source: row.source,
|
|
8474
|
+
medium: row.medium,
|
|
8475
|
+
channelGroup: row.channelGroup,
|
|
8476
|
+
sessions: row.sessions,
|
|
8477
|
+
users: row.users,
|
|
8478
|
+
syncedAt: now
|
|
8479
|
+
}).run();
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
if (syncSummary) {
|
|
8483
|
+
tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
|
|
8484
|
+
tx.insert(gaTrafficSummaries).values({
|
|
8485
|
+
id: crypto16.randomUUID(),
|
|
8486
|
+
projectId: project.id,
|
|
8487
|
+
periodStart: summary.periodStart,
|
|
8488
|
+
periodEnd: summary.periodEnd,
|
|
8489
|
+
totalSessions: summary.totalSessions,
|
|
8490
|
+
totalOrganicSessions: summary.totalOrganicSessions,
|
|
8491
|
+
totalUsers: summary.totalUsers,
|
|
8492
|
+
syncedAt: now
|
|
8493
|
+
}).run();
|
|
8494
|
+
}
|
|
8272
8495
|
});
|
|
8496
|
+
const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
|
|
8273
8497
|
gaLog("info", "sync.complete", {
|
|
8274
8498
|
projectId: project.id,
|
|
8275
8499
|
rowCount: rows.length,
|
|
8276
8500
|
aiReferralCount: aiReferrals.length,
|
|
8501
|
+
socialReferralCount: socialReferrals.length,
|
|
8277
8502
|
days,
|
|
8278
|
-
totalUsers: summary.totalUsers
|
|
8503
|
+
totalUsers: summary.totalUsers,
|
|
8504
|
+
...only ? { only } : {}
|
|
8279
8505
|
});
|
|
8280
8506
|
return {
|
|
8281
8507
|
synced: true,
|
|
8282
8508
|
rowCount: rows.length,
|
|
8283
8509
|
aiReferralCount: aiReferrals.length,
|
|
8510
|
+
socialReferralCount: socialReferrals.length,
|
|
8284
8511
|
days,
|
|
8285
|
-
syncedAt: now
|
|
8512
|
+
syncedAt: now,
|
|
8513
|
+
...syncedComponents ? { syncedComponents } : {}
|
|
8286
8514
|
};
|
|
8287
8515
|
});
|
|
8288
8516
|
app.get("/projects/:name/ga/traffic", async (request, _reply) => {
|
|
@@ -8320,9 +8548,21 @@ async function ga4Routes(app, opts) {
|
|
|
8320
8548
|
GROUP BY date, source, medium
|
|
8321
8549
|
)`
|
|
8322
8550
|
).get();
|
|
8551
|
+
const socialReferrals = app.db.select({
|
|
8552
|
+
source: gaSocialReferrals.source,
|
|
8553
|
+
medium: gaSocialReferrals.medium,
|
|
8554
|
+
channelGroup: gaSocialReferrals.channelGroup,
|
|
8555
|
+
sessions: sql3`SUM(${gaSocialReferrals.sessions})`,
|
|
8556
|
+
users: sql3`SUM(${gaSocialReferrals.users})`
|
|
8557
|
+
}).from(gaSocialReferrals).where(eq17(gaSocialReferrals.projectId, project.id)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql3`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
8558
|
+
const socialTotals = app.db.select({
|
|
8559
|
+
sessions: sql3`SUM(${gaSocialReferrals.sessions})`,
|
|
8560
|
+
users: sql3`SUM(${gaSocialReferrals.users})`
|
|
8561
|
+
}).from(gaSocialReferrals).where(eq17(gaSocialReferrals.projectId, project.id)).get();
|
|
8323
8562
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8563
|
+
const total = summary?.totalSessions ?? 0;
|
|
8324
8564
|
return {
|
|
8325
|
-
totalSessions:
|
|
8565
|
+
totalSessions: total,
|
|
8326
8566
|
totalOrganicSessions: summary?.totalOrganicSessions ?? 0,
|
|
8327
8567
|
totalUsers: summary?.totalUsers ?? 0,
|
|
8328
8568
|
topPages: rows.map((r) => ({
|
|
@@ -8340,6 +8580,18 @@ async function ga4Routes(app, opts) {
|
|
|
8340
8580
|
})),
|
|
8341
8581
|
aiSessionsDeduped: aiDeduped?.sessions ?? 0,
|
|
8342
8582
|
aiUsersDeduped: aiDeduped?.users ?? 0,
|
|
8583
|
+
socialReferrals: socialReferrals.map((r) => ({
|
|
8584
|
+
source: r.source,
|
|
8585
|
+
medium: r.medium,
|
|
8586
|
+
channelGroup: r.channelGroup,
|
|
8587
|
+
sessions: r.sessions ?? 0,
|
|
8588
|
+
users: r.users ?? 0
|
|
8589
|
+
})),
|
|
8590
|
+
socialSessions: socialTotals?.sessions ?? 0,
|
|
8591
|
+
socialUsers: socialTotals?.users ?? 0,
|
|
8592
|
+
organicSharePct: total > 0 ? Math.round((summary?.totalOrganicSessions ?? 0) / total * 100) : 0,
|
|
8593
|
+
aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
|
|
8594
|
+
socialSharePct: total > 0 ? Math.round((socialTotals?.sessions ?? 0) / total * 100) : 0,
|
|
8343
8595
|
lastSyncedAt: latestSync?.syncedAt ?? null
|
|
8344
8596
|
};
|
|
8345
8597
|
});
|
|
@@ -8356,6 +8608,146 @@ async function ga4Routes(app, opts) {
|
|
|
8356
8608
|
}).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
|
|
8357
8609
|
return rows;
|
|
8358
8610
|
});
|
|
8611
|
+
app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
|
|
8612
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8613
|
+
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8614
|
+
const rows = app.db.select({
|
|
8615
|
+
date: gaSocialReferrals.date,
|
|
8616
|
+
source: gaSocialReferrals.source,
|
|
8617
|
+
medium: gaSocialReferrals.medium,
|
|
8618
|
+
channelGroup: gaSocialReferrals.channelGroup,
|
|
8619
|
+
sessions: gaSocialReferrals.sessions,
|
|
8620
|
+
users: gaSocialReferrals.users
|
|
8621
|
+
}).from(gaSocialReferrals).where(eq17(gaSocialReferrals.projectId, project.id)).orderBy(gaSocialReferrals.date).all();
|
|
8622
|
+
return rows;
|
|
8623
|
+
});
|
|
8624
|
+
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
8625
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8626
|
+
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8627
|
+
const today = /* @__PURE__ */ new Date();
|
|
8628
|
+
const fmt = (d) => d.toISOString().split("T")[0];
|
|
8629
|
+
const daysAgo2 = (n) => {
|
|
8630
|
+
const d = new Date(today);
|
|
8631
|
+
d.setDate(d.getDate() - n);
|
|
8632
|
+
return fmt(d);
|
|
8633
|
+
};
|
|
8634
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(
|
|
8635
|
+
eq17(gaSocialReferrals.projectId, project.id),
|
|
8636
|
+
sql3`${gaSocialReferrals.date} >= ${from}`,
|
|
8637
|
+
sql3`${gaSocialReferrals.date} < ${to}`
|
|
8638
|
+
)).get();
|
|
8639
|
+
const current7d = sumSocial(daysAgo2(7), fmt(today));
|
|
8640
|
+
const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
|
|
8641
|
+
const current30d = sumSocial(daysAgo2(30), fmt(today));
|
|
8642
|
+
const prev30d = sumSocial(daysAgo2(60), daysAgo2(30));
|
|
8643
|
+
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
8644
|
+
const sourceCurrent = app.db.select({
|
|
8645
|
+
source: gaSocialReferrals.source,
|
|
8646
|
+
sessions: sql3`SUM(${gaSocialReferrals.sessions})`
|
|
8647
|
+
}).from(gaSocialReferrals).where(and6(
|
|
8648
|
+
eq17(gaSocialReferrals.projectId, project.id),
|
|
8649
|
+
sql3`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
8650
|
+
sql3`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
8651
|
+
)).groupBy(gaSocialReferrals.source).all();
|
|
8652
|
+
const sourcePrev = app.db.select({
|
|
8653
|
+
source: gaSocialReferrals.source,
|
|
8654
|
+
sessions: sql3`SUM(${gaSocialReferrals.sessions})`
|
|
8655
|
+
}).from(gaSocialReferrals).where(and6(
|
|
8656
|
+
eq17(gaSocialReferrals.projectId, project.id),
|
|
8657
|
+
sql3`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
8658
|
+
sql3`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
8659
|
+
)).groupBy(gaSocialReferrals.source).all();
|
|
8660
|
+
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
8661
|
+
let biggestMover = null;
|
|
8662
|
+
let maxDelta = 0;
|
|
8663
|
+
for (const row of sourceCurrent) {
|
|
8664
|
+
const prev = prevMap.get(row.source) ?? 0;
|
|
8665
|
+
const delta = Math.abs(row.sessions - prev);
|
|
8666
|
+
if (delta > maxDelta) {
|
|
8667
|
+
maxDelta = delta;
|
|
8668
|
+
biggestMover = {
|
|
8669
|
+
source: row.source,
|
|
8670
|
+
sessions7d: row.sessions,
|
|
8671
|
+
sessionsPrev7d: prev,
|
|
8672
|
+
changePct: pct(row.sessions, prev) ?? (row.sessions > 0 ? 100 : 0)
|
|
8673
|
+
};
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
return {
|
|
8677
|
+
socialSessions7d: current7d?.sessions ?? 0,
|
|
8678
|
+
socialSessionsPrev7d: prev7d?.sessions ?? 0,
|
|
8679
|
+
trend7dPct: pct(current7d?.sessions ?? 0, prev7d?.sessions ?? 0),
|
|
8680
|
+
socialSessions30d: current30d?.sessions ?? 0,
|
|
8681
|
+
socialSessionsPrev30d: prev30d?.sessions ?? 0,
|
|
8682
|
+
trend30dPct: pct(current30d?.sessions ?? 0, prev30d?.sessions ?? 0),
|
|
8683
|
+
biggestMover
|
|
8684
|
+
};
|
|
8685
|
+
});
|
|
8686
|
+
app.get("/projects/:name/ga/attribution-trend", async (request, _reply) => {
|
|
8687
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8688
|
+
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8689
|
+
const today = /* @__PURE__ */ new Date();
|
|
8690
|
+
const fmt = (d) => d.toISOString().split("T")[0];
|
|
8691
|
+
const daysAgo2 = (n) => {
|
|
8692
|
+
const d = new Date(today);
|
|
8693
|
+
d.setDate(d.getDate() - n);
|
|
8694
|
+
return fmt(d);
|
|
8695
|
+
};
|
|
8696
|
+
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
8697
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql3`${gaTrafficSnapshots.date} >= ${from}`, sql3`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8698
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql3`${gaTrafficSnapshots.date} >= ${from}`, sql3`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8699
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(max_sessions), 0)` }).from(sql3`(
|
|
8700
|
+
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8701
|
+
FROM ga_ai_referrals
|
|
8702
|
+
WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
|
|
8703
|
+
GROUP BY date, source, medium
|
|
8704
|
+
)`).get();
|
|
8705
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql3`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql3`${gaSocialReferrals.date} >= ${from}`, sql3`${gaSocialReferrals.date} < ${to}`)).get();
|
|
8706
|
+
const todayStr = fmt(today);
|
|
8707
|
+
const buildTrend = (sum) => {
|
|
8708
|
+
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
8709
|
+
const p7 = sum(daysAgo2(14), daysAgo2(7))?.sessions ?? 0;
|
|
8710
|
+
const c30 = sum(daysAgo2(30), todayStr)?.sessions ?? 0;
|
|
8711
|
+
const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
|
|
8712
|
+
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
8713
|
+
};
|
|
8714
|
+
const aiSourceCurrent = app.db.select({ source: sql3`source`, sessions: sql3`COALESCE(SUM(max_sessions), 0)` }).from(sql3`(
|
|
8715
|
+
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8716
|
+
FROM ga_ai_referrals
|
|
8717
|
+
WHERE project_id = ${project.id} AND date >= ${daysAgo2(7)} AND date < ${todayStr}
|
|
8718
|
+
GROUP BY date, source, medium
|
|
8719
|
+
)`).groupBy(sql3`source`).all();
|
|
8720
|
+
const aiSourcePrev = app.db.select({ source: sql3`source`, sessions: sql3`COALESCE(SUM(max_sessions), 0)` }).from(sql3`(
|
|
8721
|
+
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8722
|
+
FROM ga_ai_referrals
|
|
8723
|
+
WHERE project_id = ${project.id} AND date >= ${daysAgo2(14)} AND date < ${daysAgo2(7)}
|
|
8724
|
+
GROUP BY date, source, medium
|
|
8725
|
+
)`).groupBy(sql3`source`).all();
|
|
8726
|
+
const findBiggestMover = (current, prev) => {
|
|
8727
|
+
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
8728
|
+
let mover = null;
|
|
8729
|
+
let maxDelta = 0;
|
|
8730
|
+
for (const row of current) {
|
|
8731
|
+
const p = prevMap.get(row.source) ?? 0;
|
|
8732
|
+
const delta = Math.abs(row.sessions - p);
|
|
8733
|
+
if (delta > maxDelta) {
|
|
8734
|
+
maxDelta = delta;
|
|
8735
|
+
mover = { source: row.source, sessions7d: row.sessions, sessionsPrev7d: p, changePct: pct(row.sessions, p) ?? (row.sessions > 0 ? 100 : 0) };
|
|
8736
|
+
}
|
|
8737
|
+
}
|
|
8738
|
+
return mover;
|
|
8739
|
+
};
|
|
8740
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql3`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql3`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql3`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
8741
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql3`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql3`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql3`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
8742
|
+
return {
|
|
8743
|
+
total: buildTrend(sumTotal),
|
|
8744
|
+
organic: buildTrend(sumOrganic),
|
|
8745
|
+
ai: buildTrend(sumAi),
|
|
8746
|
+
social: buildTrend(sumSocial),
|
|
8747
|
+
aiBiggestMover: findBiggestMover(aiSourceCurrent, aiSourcePrev),
|
|
8748
|
+
socialBiggestMover: findBiggestMover(socialSourceCurrent, socialSourcePrev)
|
|
8749
|
+
};
|
|
8750
|
+
});
|
|
8359
8751
|
app.get("/projects/:name/ga/session-history", async (request, _reply) => {
|
|
8360
8752
|
const project = resolveProject(app.db, request.params.name);
|
|
8361
8753
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|