@ainyc/canonry 4.115.0 → 4.115.1
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/{BacklinksPage-DhXsZ8Sb.js → BacklinksPage-yUb8oJuq.js} +1 -1
- package/assets/assets/{ChartPrimitives-Cdj0P8uG.js → ChartPrimitives-CdQM3YDG.js} +1 -1
- package/assets/assets/ProjectPage-CHj9fvTs.js +7 -0
- package/assets/assets/{RunRow-GJ_2CEif.js → RunRow-BU_x7EcP.js} +1 -1
- package/assets/assets/{RunsPage-upaDiLFK.js → RunsPage-CFIBWaO7.js} +1 -1
- package/assets/assets/{SettingsPage-FbmEaN16.js → SettingsPage-PyMryUad.js} +1 -1
- package/assets/assets/{TrafficPage-Di0H5JRu.js → TrafficPage-471ypAdQ.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-Bc2mxXSV.js → TrafficSourceDetailPage-CI7TEQge.js} +1 -1
- package/assets/assets/{arrow-left-yOCIy3_x.js → arrow-left-D2N9u-0i.js} +1 -1
- package/assets/assets/{extract-error-message-tCNIzznL.js → extract-error-message-CAdsqKp3.js} +1 -1
- package/assets/assets/{index-Bb5yUz7k.css → index-B4R1-8hR.css} +1 -1
- package/assets/assets/{index-D0ZdnFQf.js → index-CoL5HENE.js} +66 -66
- package/assets/assets/{trash-2-gRyTO-PS.js → trash-2-UgQrN5Np.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-VEUBAZRG.js → chunk-6LOKMEL3.js} +101 -0
- package/dist/{chunk-PGVGHGIX.js → chunk-B2SNXXAJ.js} +4 -4
- package/dist/{chunk-MO7GPMNV.js → chunk-CW4JISBL.js} +1 -1
- package/dist/{chunk-BXNFOGDF.js → chunk-Z3OGDQHB.js} +268 -63
- package/dist/cli.js +52 -8
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-2C7H4C6A.js → intelligence-service-4AWB2V7U.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
- package/assets/assets/ProjectPage-DAWtJQ3w.js +0 -7
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
DiscoveryBuckets,
|
|
21
21
|
DiscoveryCompetitorTypes,
|
|
22
22
|
DiscoverySessionStatuses,
|
|
23
|
+
GA4AiReferralTrafficClasses,
|
|
23
24
|
RunKinds,
|
|
24
25
|
RunStatuses,
|
|
25
26
|
RunTriggers,
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
citationStateToCited,
|
|
88
89
|
citationVisibilityResponseSchema,
|
|
89
90
|
classifyCitedSurface,
|
|
91
|
+
classifyGa4AiReferralTrafficClass,
|
|
90
92
|
classifySkillFile,
|
|
91
93
|
classifySurfaceFromCategory,
|
|
92
94
|
classifyTrafficPath,
|
|
@@ -273,7 +275,7 @@ import {
|
|
|
273
275
|
wordpressSchemaDeployResultDtoSchema,
|
|
274
276
|
wordpressSchemaStatusResultDtoSchema,
|
|
275
277
|
wordpressStatusDtoSchema
|
|
276
|
-
} from "./chunk-
|
|
278
|
+
} from "./chunk-6LOKMEL3.js";
|
|
277
279
|
|
|
278
280
|
// src/intelligence-service.ts
|
|
279
281
|
import { eq as eq38, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
@@ -758,6 +760,7 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
|
|
|
758
760
|
date: text("date").notNull(),
|
|
759
761
|
source: text("source").notNull(),
|
|
760
762
|
medium: text("medium").notNull(),
|
|
763
|
+
trafficClass: text("traffic_class").$type().notNull().default("organic"),
|
|
761
764
|
/** Which GA4 dimension produced this row: 'session' | 'first_user' | 'manual_utm' */
|
|
762
765
|
sourceDimension: text("source_dimension").notNull().default("session"),
|
|
763
766
|
/** GA4 default channel group for the session (e.g. 'Referral', 'Organic Social'). */
|
|
@@ -772,6 +775,7 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
|
|
|
772
775
|
index("idx_ga_ai_ref_project_date").on(table.projectId, table.date),
|
|
773
776
|
index("idx_ga_ai_ref_source").on(table.source),
|
|
774
777
|
index("idx_ga_ai_ref_landing_page").on(table.projectId, table.date, table.landingPageNormalized),
|
|
778
|
+
index("idx_ga_ai_ref_traffic_class").on(table.projectId, table.date, table.trafficClass),
|
|
775
779
|
uniqueIndex("idx_ga_ai_ref_unique_v4").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension, table.channelGroup, table.landingPage),
|
|
776
780
|
index("idx_ga_ai_ref_run").on(table.syncRunId)
|
|
777
781
|
]);
|
|
@@ -3482,6 +3486,41 @@ var MIGRATION_VERSIONS = [
|
|
|
3482
3486
|
// post-truncation). Additive + nullable (downgrade-safe).
|
|
3483
3487
|
`ALTER TABLE discovery_sessions ADD COLUMN canonical_count INTEGER`
|
|
3484
3488
|
]
|
|
3489
|
+
},
|
|
3490
|
+
{
|
|
3491
|
+
version: 95,
|
|
3492
|
+
name: "ga-ai-referral-traffic-class",
|
|
3493
|
+
statements: [],
|
|
3494
|
+
run: (tx) => {
|
|
3495
|
+
if (!tableExists(tx, "ga_ai_referrals")) return;
|
|
3496
|
+
if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) {
|
|
3497
|
+
tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
|
|
3498
|
+
}
|
|
3499
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_traffic_class ON ga_ai_referrals(project_id, date, traffic_class)`));
|
|
3500
|
+
}
|
|
3501
|
+
},
|
|
3502
|
+
{
|
|
3503
|
+
// v95 added `ga_ai_referrals.traffic_class` with `DEFAULT 'organic'` and did
|
|
3504
|
+
// not classify the rows that already existed. Every historical AI referral
|
|
3505
|
+
// was therefore stamped 'organic', including paid ChatGPT-ads traffic
|
|
3506
|
+
// carrying `medium='cpc'` / `channel_group='Paid Other'`. That silently
|
|
3507
|
+
// reports purchased sessions as organic wins.
|
|
3508
|
+
//
|
|
3509
|
+
// The class is a pure function of columns already stored on each row, so
|
|
3510
|
+
// re-derive it here instead of making an operator delete and re-fetch
|
|
3511
|
+
// identical data from GA4.
|
|
3512
|
+
//
|
|
3513
|
+
// Every row is re-classified, not only the "unclassified" ones: v95's
|
|
3514
|
+
// default is indistinguishable from a genuinely-organic classification, so
|
|
3515
|
+
// there is no way to tell the two apart. `classifyGa4AiReferralTrafficClass`
|
|
3516
|
+
// is pure and deterministic, so re-running it over already-correct rows
|
|
3517
|
+
// writes nothing, which is what makes this version safe to replay.
|
|
3518
|
+
version: 96,
|
|
3519
|
+
name: "ga-ai-referral-traffic-class-backfill",
|
|
3520
|
+
statements: [],
|
|
3521
|
+
run: (tx) => {
|
|
3522
|
+
backfillGaAiReferralTrafficClass(tx);
|
|
3523
|
+
}
|
|
3485
3524
|
}
|
|
3486
3525
|
];
|
|
3487
3526
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -3568,6 +3607,26 @@ function tableIsEmpty(db, table) {
|
|
|
3568
3607
|
const rows = db.all(sql.raw(`SELECT COUNT(*) as c FROM ${table}`));
|
|
3569
3608
|
return (rows[0]?.c ?? 0) === 0;
|
|
3570
3609
|
}
|
|
3610
|
+
function backfillGaAiReferralTrafficClass(tx) {
|
|
3611
|
+
if (!tableExists(tx, "ga_ai_referrals")) return 0;
|
|
3612
|
+
if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) return 0;
|
|
3613
|
+
const rows = tx.all(sql.raw(
|
|
3614
|
+
`SELECT id, source, medium, channel_group, landing_page, traffic_class FROM ga_ai_referrals`
|
|
3615
|
+
));
|
|
3616
|
+
let updated = 0;
|
|
3617
|
+
for (const row of rows) {
|
|
3618
|
+
const next = classifyGa4AiReferralTrafficClass({
|
|
3619
|
+
source: row.source,
|
|
3620
|
+
medium: row.medium,
|
|
3621
|
+
channelGroup: row.channel_group,
|
|
3622
|
+
landingPage: row.landing_page
|
|
3623
|
+
});
|
|
3624
|
+
if (next === row.traffic_class) continue;
|
|
3625
|
+
tx.run(sql`UPDATE ga_ai_referrals SET traffic_class = ${next} WHERE id = ${row.id}`);
|
|
3626
|
+
updated += 1;
|
|
3627
|
+
}
|
|
3628
|
+
return updated;
|
|
3629
|
+
}
|
|
3571
3630
|
function hasLegacyQuerySchema(db) {
|
|
3572
3631
|
return tableExists(db, "keywords") || columnExists(db, "query_snapshots", "keyword_id") || columnExists(db, "insights", "keyword");
|
|
3573
3632
|
}
|
|
@@ -8137,7 +8196,7 @@ async function analyticsRoutes(app) {
|
|
|
8137
8196
|
const project = resolveProject(app.db, request.params.name);
|
|
8138
8197
|
const window = parseWindow(request.query.window);
|
|
8139
8198
|
const cutoff = windowCutoff(window);
|
|
8140
|
-
const projectRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8199
|
+
const projectRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8141
8200
|
if (projectRuns.length === 0) {
|
|
8142
8201
|
return reply.send({
|
|
8143
8202
|
window,
|
|
@@ -8189,14 +8248,14 @@ async function analyticsRoutes(app) {
|
|
|
8189
8248
|
const project = resolveProject(app.db, request.params.name);
|
|
8190
8249
|
const window = parseWindow(request.query.window);
|
|
8191
8250
|
const cutoff = windowCutoff(window);
|
|
8192
|
-
const completedRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
|
|
8251
|
+
const completedRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
|
|
8193
8252
|
const latestGroup = groupRunsByCreatedAt(completedRuns)[0] ?? [];
|
|
8194
8253
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
8195
8254
|
const latestRun = pickGroupRepresentative(latestGroup);
|
|
8196
8255
|
if (!latestRun) {
|
|
8197
8256
|
return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
|
|
8198
8257
|
}
|
|
8199
|
-
const windowRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8258
|
+
const windowRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8200
8259
|
const windowRunIds = windowRuns.map((r) => r.id);
|
|
8201
8260
|
const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
|
|
8202
8261
|
const consistencyMap = /* @__PURE__ */ new Map();
|
|
@@ -8327,7 +8386,7 @@ async function analyticsRoutes(app) {
|
|
|
8327
8386
|
const mapped = surfaceClassFromCompetitorType(row.competitorType);
|
|
8328
8387
|
if (mapped) storedSurfaceClasses.set(normalizeProjectDomain(row.domain), mapped);
|
|
8329
8388
|
}
|
|
8330
|
-
const windowRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8389
|
+
const windowRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8331
8390
|
if (windowRuns.length === 0) {
|
|
8332
8391
|
return reply.send({
|
|
8333
8392
|
overall: [],
|
|
@@ -11845,6 +11904,9 @@ function safeNum(value) {
|
|
|
11845
11904
|
}
|
|
11846
11905
|
return 0;
|
|
11847
11906
|
}
|
|
11907
|
+
function isPaidAiTrafficClass(value) {
|
|
11908
|
+
return value === GA4AiReferralTrafficClasses.paid;
|
|
11909
|
+
}
|
|
11848
11910
|
function categorizeQuery(query, projectBrandNames, canonicalDomain) {
|
|
11849
11911
|
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
|
|
11850
11912
|
}
|
|
@@ -11999,14 +12061,40 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
11999
12061
|
users: data.users,
|
|
12000
12062
|
organicSessions: data.organic
|
|
12001
12063
|
})).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_LANDING_PAGES_LIMIT);
|
|
12064
|
+
const aiConditions = [
|
|
12065
|
+
eq15(gaAiReferrals.projectId, projectId),
|
|
12066
|
+
eq15(gaAiReferrals.sourceDimension, "session")
|
|
12067
|
+
];
|
|
12068
|
+
if (snapshotStartDate && snapshotMaxDate) {
|
|
12069
|
+
aiConditions.push(gte2(gaAiReferrals.date, snapshotStartDate));
|
|
12070
|
+
aiConditions.push(lte(gaAiReferrals.date, snapshotMaxDate));
|
|
12071
|
+
}
|
|
12072
|
+
const aiSessionRows = db.select({
|
|
12073
|
+
trafficClass: gaAiReferrals.trafficClass,
|
|
12074
|
+
channelGroup: gaAiReferrals.channelGroup,
|
|
12075
|
+
sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`
|
|
12076
|
+
}).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.trafficClass, gaAiReferrals.channelGroup).all();
|
|
12077
|
+
let paidAiSessions = 0;
|
|
12078
|
+
let organicAiSessions = 0;
|
|
12079
|
+
let aiOrganicOverlap = 0;
|
|
12080
|
+
let aiDirectOverlap = 0;
|
|
12081
|
+
for (const row of aiSessionRows) {
|
|
12082
|
+
const sessions = Number(row.sessions ?? 0);
|
|
12083
|
+
if (isPaidAiTrafficClass(row.trafficClass)) paidAiSessions += sessions;
|
|
12084
|
+
else organicAiSessions += sessions;
|
|
12085
|
+
if (row.channelGroup === "Organic Search") aiOrganicOverlap += sessions;
|
|
12086
|
+
if (row.channelGroup === "Direct") aiDirectOverlap += sessions;
|
|
12087
|
+
}
|
|
12002
12088
|
const channelBreakdown = [];
|
|
12003
12089
|
if (totalSessions > 0) {
|
|
12004
|
-
const organic = totalOrganicSessions;
|
|
12005
|
-
const direct = directSessions;
|
|
12006
|
-
const other = Math.max(totalSessions - organic - direct, 0);
|
|
12090
|
+
const organic = Math.max(0, totalOrganicSessions - Math.min(totalOrganicSessions, aiOrganicOverlap));
|
|
12091
|
+
const direct = Math.max(0, directSessions - Math.min(directSessions, aiDirectOverlap));
|
|
12092
|
+
const other = Math.max(totalSessions - organic - direct - paidAiSessions - organicAiSessions, 0);
|
|
12007
12093
|
const buckets = [
|
|
12008
12094
|
{ channel: "Organic Search", sessions: organic },
|
|
12009
12095
|
{ channel: "Direct", sessions: direct },
|
|
12096
|
+
{ channel: "Paid AI", sessions: paidAiSessions },
|
|
12097
|
+
{ channel: "Organic AI referrals", sessions: organicAiSessions },
|
|
12010
12098
|
{ channel: "Other", sessions: other }
|
|
12011
12099
|
];
|
|
12012
12100
|
for (const b of buckets) {
|
|
@@ -12093,15 +12181,34 @@ function buildAiReferrals(db, projectId) {
|
|
|
12093
12181
|
);
|
|
12094
12182
|
let total = 0;
|
|
12095
12183
|
let totalUsers = 0;
|
|
12184
|
+
let paidSessions = 0;
|
|
12185
|
+
let paidUsers = 0;
|
|
12186
|
+
let organicSessions = 0;
|
|
12187
|
+
let organicUsers = 0;
|
|
12096
12188
|
const sourceAgg = /* @__PURE__ */ new Map();
|
|
12097
12189
|
const trendAgg = /* @__PURE__ */ new Map();
|
|
12098
12190
|
const pageAgg = /* @__PURE__ */ new Map();
|
|
12099
12191
|
for (const r of dedupedRows) {
|
|
12100
12192
|
total += r.sessions;
|
|
12101
12193
|
totalUsers += r.users;
|
|
12102
|
-
const
|
|
12194
|
+
const paid = isPaidAiTrafficClass(r.trafficClass);
|
|
12195
|
+
if (paid) {
|
|
12196
|
+
paidSessions += r.sessions;
|
|
12197
|
+
paidUsers += r.users;
|
|
12198
|
+
} else {
|
|
12199
|
+
organicSessions += r.sessions;
|
|
12200
|
+
organicUsers += r.users;
|
|
12201
|
+
}
|
|
12202
|
+
const s = sourceAgg.get(r.source) ?? {
|
|
12203
|
+
sessions: 0,
|
|
12204
|
+
users: 0,
|
|
12205
|
+
paidSessions: 0,
|
|
12206
|
+
organicSessions: 0
|
|
12207
|
+
};
|
|
12103
12208
|
s.sessions += r.sessions;
|
|
12104
12209
|
s.users += r.users;
|
|
12210
|
+
if (paid) s.paidSessions += r.sessions;
|
|
12211
|
+
else s.organicSessions += r.sessions;
|
|
12105
12212
|
sourceAgg.set(r.source, s);
|
|
12106
12213
|
trendAgg.set(r.date, (trendAgg.get(r.date) ?? 0) + r.sessions);
|
|
12107
12214
|
const page = r.landingPageNormalized ?? r.landingPage;
|
|
@@ -12114,11 +12221,23 @@ function buildAiReferrals(db, projectId) {
|
|
|
12114
12221
|
source,
|
|
12115
12222
|
sessions: data.sessions,
|
|
12116
12223
|
users: data.users,
|
|
12224
|
+
paidSessions: data.paidSessions,
|
|
12225
|
+
organicSessions: data.organicSessions,
|
|
12117
12226
|
sharePct: total > 0 ? Math.round(data.sessions / total * 100) : 0
|
|
12118
12227
|
})).sort((a, b) => b.sessions - a.sessions);
|
|
12119
12228
|
const trend = [...trendAgg.entries()].map(([date, sessions]) => ({ date, sessions })).sort((a, b) => a.date.localeCompare(b.date));
|
|
12120
12229
|
const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({ page, sessions: data.sessions, users: data.users })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
|
|
12121
|
-
return {
|
|
12230
|
+
return {
|
|
12231
|
+
totalSessions: total,
|
|
12232
|
+
totalUsers,
|
|
12233
|
+
paidSessions,
|
|
12234
|
+
paidUsers,
|
|
12235
|
+
organicSessions,
|
|
12236
|
+
organicUsers,
|
|
12237
|
+
bySource,
|
|
12238
|
+
trend,
|
|
12239
|
+
topLandingPages
|
|
12240
|
+
};
|
|
12122
12241
|
}
|
|
12123
12242
|
function nonSubresourceReferralPathCondition() {
|
|
12124
12243
|
return sql7`
|
|
@@ -20423,16 +20542,23 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
|
|
|
20423
20542
|
};
|
|
20424
20543
|
const response = await runReport(accessToken, propertyId, request);
|
|
20425
20544
|
if (!response) break;
|
|
20426
|
-
const pageRows = (response.rows ?? []).map((row) =>
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20545
|
+
const pageRows = (response.rows ?? []).map((row) => {
|
|
20546
|
+
const source = row.dimensionValues[1].value;
|
|
20547
|
+
const medium = row.dimensionValues[2].value;
|
|
20548
|
+
const channelGroup = row.dimensionValues[3]?.value ?? "(not set)";
|
|
20549
|
+
const landingPage = row.dimensionValues[4]?.value ?? "(not set)";
|
|
20550
|
+
return {
|
|
20551
|
+
date: row.dimensionValues[0].value,
|
|
20552
|
+
source,
|
|
20553
|
+
medium,
|
|
20554
|
+
trafficClass: classifyGa4AiReferralTrafficClass({ source, medium, channelGroup, landingPage }),
|
|
20555
|
+
channelGroup,
|
|
20556
|
+
landingPage,
|
|
20557
|
+
sessions: parseInt(row.metricValues[0].value, 10) || 0,
|
|
20558
|
+
users: parseInt(row.metricValues[1].value, 10) || 0,
|
|
20559
|
+
sourceDimension: dimLabel
|
|
20560
|
+
};
|
|
20561
|
+
});
|
|
20436
20562
|
rows.push(...pageRows);
|
|
20437
20563
|
const totalRows = response.rowCount ?? 0;
|
|
20438
20564
|
offset += pageRows.length;
|
|
@@ -23400,6 +23526,76 @@ function pickWinningDimension(rows, tupleKey) {
|
|
|
23400
23526
|
}
|
|
23401
23527
|
return [...winners.values()].sort((a, b) => (b.sessions ?? 0) - (a.sessions ?? 0));
|
|
23402
23528
|
}
|
|
23529
|
+
function normalizeAiTrafficClass(value) {
|
|
23530
|
+
return value === GA4AiReferralTrafficClasses.paid ? GA4AiReferralTrafficClasses.paid : GA4AiReferralTrafficClasses.organic;
|
|
23531
|
+
}
|
|
23532
|
+
function emptyAiCounts() {
|
|
23533
|
+
return { sessions: 0, users: 0 };
|
|
23534
|
+
}
|
|
23535
|
+
function summarizeAiReferralCounts(rows) {
|
|
23536
|
+
const dedupeGroups = /* @__PURE__ */ new Map();
|
|
23537
|
+
const bySessionChannelGroup = /* @__PURE__ */ new Map();
|
|
23538
|
+
const paidDeduped = emptyAiCounts();
|
|
23539
|
+
const organicDeduped = emptyAiCounts();
|
|
23540
|
+
const paidBySession = emptyAiCounts();
|
|
23541
|
+
const organicBySession = emptyAiCounts();
|
|
23542
|
+
for (const row of rows) {
|
|
23543
|
+
const isPaid = normalizeAiTrafficClass(row.trafficClass) === GA4AiReferralTrafficClasses.paid;
|
|
23544
|
+
const sessions = row.sessions ?? 0;
|
|
23545
|
+
const users = row.users ?? 0;
|
|
23546
|
+
const key = `${row.date}\0${row.source}\0${row.medium}`;
|
|
23547
|
+
let byDimension = dedupeGroups.get(key);
|
|
23548
|
+
if (!byDimension) {
|
|
23549
|
+
byDimension = /* @__PURE__ */ new Map();
|
|
23550
|
+
dedupeGroups.set(key, byDimension);
|
|
23551
|
+
}
|
|
23552
|
+
let dim = byDimension.get(row.sourceDimension);
|
|
23553
|
+
if (!dim) {
|
|
23554
|
+
dim = { paidSessions: 0, organicSessions: 0, paidUsers: 0, organicUsers: 0 };
|
|
23555
|
+
byDimension.set(row.sourceDimension, dim);
|
|
23556
|
+
}
|
|
23557
|
+
if (isPaid) {
|
|
23558
|
+
dim.paidSessions += sessions;
|
|
23559
|
+
dim.paidUsers += users;
|
|
23560
|
+
} else {
|
|
23561
|
+
dim.organicSessions += sessions;
|
|
23562
|
+
dim.organicUsers += users;
|
|
23563
|
+
}
|
|
23564
|
+
if (row.sourceDimension === "session") {
|
|
23565
|
+
bySessionChannelGroup.set(
|
|
23566
|
+
row.channelGroup,
|
|
23567
|
+
(bySessionChannelGroup.get(row.channelGroup) ?? 0) + sessions
|
|
23568
|
+
);
|
|
23569
|
+
const sessionBucket = isPaid ? paidBySession : organicBySession;
|
|
23570
|
+
sessionBucket.sessions += sessions;
|
|
23571
|
+
sessionBucket.users += users;
|
|
23572
|
+
}
|
|
23573
|
+
}
|
|
23574
|
+
for (const byDimension of dedupeGroups.values()) {
|
|
23575
|
+
const dims = [...byDimension.values()];
|
|
23576
|
+
const bestSessions = dims.reduce((best, d) => d.paidSessions + d.organicSessions > best.paidSessions + best.organicSessions ? d : best);
|
|
23577
|
+
const bestUsers = dims.reduce((best, d) => d.paidUsers + d.organicUsers > best.paidUsers + best.organicUsers ? d : best);
|
|
23578
|
+
paidDeduped.sessions += bestSessions.paidSessions;
|
|
23579
|
+
organicDeduped.sessions += bestSessions.organicSessions;
|
|
23580
|
+
paidDeduped.users += bestUsers.paidUsers;
|
|
23581
|
+
organicDeduped.users += bestUsers.organicUsers;
|
|
23582
|
+
}
|
|
23583
|
+
return {
|
|
23584
|
+
paidDeduped,
|
|
23585
|
+
organicDeduped,
|
|
23586
|
+
paidBySession,
|
|
23587
|
+
organicBySession,
|
|
23588
|
+
bySessionChannelGroup,
|
|
23589
|
+
deduped: {
|
|
23590
|
+
sessions: paidDeduped.sessions + organicDeduped.sessions,
|
|
23591
|
+
users: paidDeduped.users + organicDeduped.users
|
|
23592
|
+
},
|
|
23593
|
+
bySession: {
|
|
23594
|
+
sessions: paidBySession.sessions + organicBySession.sessions,
|
|
23595
|
+
users: paidBySession.users + organicBySession.users
|
|
23596
|
+
}
|
|
23597
|
+
};
|
|
23598
|
+
}
|
|
23403
23599
|
async function refreshOAuthTokenIfNeeded(googleStore, authConfig, canonicalDomain, oauthConn) {
|
|
23404
23600
|
const expiresAt = oauthConn.tokenExpiresAt ? new Date(oauthConn.tokenExpiresAt).getTime() : 0;
|
|
23405
23601
|
const fiveMinutes = 5 * 60 * 1e3;
|
|
@@ -23676,6 +23872,12 @@ async function ga4Routes(app, opts) {
|
|
|
23676
23872
|
date: row.date,
|
|
23677
23873
|
source: row.source,
|
|
23678
23874
|
medium: row.medium,
|
|
23875
|
+
trafficClass: row.trafficClass ?? classifyGa4AiReferralTrafficClass({
|
|
23876
|
+
source: row.source,
|
|
23877
|
+
medium: row.medium,
|
|
23878
|
+
channelGroup: row.channelGroup,
|
|
23879
|
+
landingPage: row.landingPage
|
|
23880
|
+
}),
|
|
23679
23881
|
sourceDimension: row.sourceDimension,
|
|
23680
23882
|
channelGroup: row.channelGroup,
|
|
23681
23883
|
landingPage: row.landingPage,
|
|
@@ -23825,13 +24027,15 @@ async function ga4Routes(app, opts) {
|
|
|
23825
24027
|
const aiReferralRows = app.db.select({
|
|
23826
24028
|
source: gaAiReferrals.source,
|
|
23827
24029
|
medium: gaAiReferrals.medium,
|
|
24030
|
+
trafficClass: gaAiReferrals.trafficClass,
|
|
23828
24031
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23829
24032
|
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23830
24033
|
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23831
|
-
}).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
24034
|
+
}).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
|
|
23832
24035
|
const aiReferralLandingPageRows = app.db.select({
|
|
23833
24036
|
source: gaAiReferrals.source,
|
|
23834
24037
|
medium: gaAiReferrals.medium,
|
|
24038
|
+
trafficClass: gaAiReferrals.trafficClass,
|
|
23835
24039
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23836
24040
|
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
23837
24041
|
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
@@ -23839,48 +24043,29 @@ async function ga4Routes(app, opts) {
|
|
|
23839
24043
|
}).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(
|
|
23840
24044
|
gaAiReferrals.source,
|
|
23841
24045
|
gaAiReferrals.medium,
|
|
24046
|
+
gaAiReferrals.trafficClass,
|
|
23842
24047
|
gaAiReferrals.sourceDimension,
|
|
23843
24048
|
sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
23844
24049
|
).all();
|
|
24050
|
+
const aiRowsForTotals = app.db.select({
|
|
24051
|
+
date: gaAiReferrals.date,
|
|
24052
|
+
source: gaAiReferrals.source,
|
|
24053
|
+
medium: gaAiReferrals.medium,
|
|
24054
|
+
trafficClass: gaAiReferrals.trafficClass,
|
|
24055
|
+
sourceDimension: gaAiReferrals.sourceDimension,
|
|
24056
|
+
channelGroup: gaAiReferrals.channelGroup,
|
|
24057
|
+
sessions: gaAiReferrals.sessions,
|
|
24058
|
+
users: gaAiReferrals.users
|
|
24059
|
+
}).from(gaAiReferrals).where(and21(...aiConditions)).all();
|
|
23845
24060
|
const aiReferrals = pickWinningDimension(
|
|
23846
24061
|
aiReferralRows,
|
|
23847
|
-
(r) => `${r.source}\0${r.medium}`
|
|
24062
|
+
(r) => `${r.source}\0${r.medium}\0${r.trafficClass}`
|
|
23848
24063
|
);
|
|
23849
24064
|
const aiReferralLandingPages = pickWinningDimension(
|
|
23850
24065
|
aiReferralLandingPageRows,
|
|
23851
|
-
(r) => `${r.source}\0${r.medium}\0${r.landingPage}`
|
|
24066
|
+
(r) => `${r.source}\0${r.medium}\0${r.trafficClass}\0${r.landingPage}`
|
|
23852
24067
|
);
|
|
23853
|
-
const
|
|
23854
|
-
sessions: sql10`COALESCE(SUM(max_sessions), 0)`,
|
|
23855
|
-
users: sql10`COALESCE(SUM(max_users), 0)`
|
|
23856
|
-
}).from(
|
|
23857
|
-
sql10`(
|
|
23858
|
-
SELECT date, source, medium,
|
|
23859
|
-
MAX(dimension_sessions) AS max_sessions,
|
|
23860
|
-
MAX(dimension_users) AS max_users
|
|
23861
|
-
FROM (
|
|
23862
|
-
SELECT date, source, medium, source_dimension,
|
|
23863
|
-
SUM(sessions) AS dimension_sessions,
|
|
23864
|
-
SUM(users) AS dimension_users
|
|
23865
|
-
FROM ga_ai_referrals
|
|
23866
|
-
WHERE project_id = ${project.id}${cutoffDate ? sql10` AND date >= ${cutoffDate}` : sql10``}
|
|
23867
|
-
GROUP BY date, source, medium, source_dimension
|
|
23868
|
-
)
|
|
23869
|
-
GROUP BY date, source, medium
|
|
23870
|
-
)`
|
|
23871
|
-
).get();
|
|
23872
|
-
const aiBySessionRows = app.db.select({
|
|
23873
|
-
channelGroup: gaAiReferrals.channelGroup,
|
|
23874
|
-
sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
23875
|
-
users: sql10`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
23876
|
-
}).from(gaAiReferrals).where(and21(...aiConditions, eq26(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
23877
|
-
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
23878
|
-
let aiBySessionUsers = 0;
|
|
23879
|
-
for (const row of aiBySessionRows) {
|
|
23880
|
-
aiSessionsByChannelGroup.set(row.channelGroup, row.sessions ?? 0);
|
|
23881
|
-
aiBySessionUsers += row.users ?? 0;
|
|
23882
|
-
}
|
|
23883
|
-
const aiBySessionSessions = [...aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
|
|
24068
|
+
const aiSummary = summarizeAiReferralCounts(aiRowsForTotals);
|
|
23884
24069
|
const socialReferrals = app.db.select({
|
|
23885
24070
|
source: gaSocialReferrals.source,
|
|
23886
24071
|
medium: gaSocialReferrals.medium,
|
|
@@ -23902,7 +24087,7 @@ async function ga4Routes(app, opts) {
|
|
|
23902
24087
|
organicSessions: totalOrganicSessions,
|
|
23903
24088
|
socialSessions,
|
|
23904
24089
|
directSessions: totalDirectSessions,
|
|
23905
|
-
aiSessionsByChannelGroup
|
|
24090
|
+
aiSessionsByChannelGroup: aiSummary.bySessionChannelGroup
|
|
23906
24091
|
});
|
|
23907
24092
|
return {
|
|
23908
24093
|
totalSessions: total,
|
|
@@ -23919,6 +24104,7 @@ async function ga4Routes(app, opts) {
|
|
|
23919
24104
|
aiReferrals: aiReferrals.map((r) => ({
|
|
23920
24105
|
source: r.source,
|
|
23921
24106
|
medium: r.medium,
|
|
24107
|
+
trafficClass: normalizeAiTrafficClass(r.trafficClass),
|
|
23922
24108
|
sourceDimension: r.sourceDimension,
|
|
23923
24109
|
sessions: r.sessions ?? 0,
|
|
23924
24110
|
users: r.users ?? 0
|
|
@@ -23926,15 +24112,24 @@ async function ga4Routes(app, opts) {
|
|
|
23926
24112
|
aiReferralLandingPages: aiReferralLandingPages.map((r) => ({
|
|
23927
24113
|
source: r.source,
|
|
23928
24114
|
medium: r.medium,
|
|
24115
|
+
trafficClass: normalizeAiTrafficClass(r.trafficClass),
|
|
23929
24116
|
sourceDimension: r.sourceDimension,
|
|
23930
24117
|
landingPage: r.landingPage,
|
|
23931
24118
|
sessions: r.sessions ?? 0,
|
|
23932
24119
|
users: r.users ?? 0
|
|
23933
24120
|
})),
|
|
23934
|
-
aiSessionsDeduped:
|
|
23935
|
-
aiUsersDeduped:
|
|
23936
|
-
|
|
23937
|
-
|
|
24121
|
+
aiSessionsDeduped: aiSummary.deduped.sessions,
|
|
24122
|
+
aiUsersDeduped: aiSummary.deduped.users,
|
|
24123
|
+
paidAiSessionsDeduped: aiSummary.paidDeduped.sessions,
|
|
24124
|
+
paidAiUsersDeduped: aiSummary.paidDeduped.users,
|
|
24125
|
+
organicAiSessionsDeduped: aiSummary.organicDeduped.sessions,
|
|
24126
|
+
organicAiUsersDeduped: aiSummary.organicDeduped.users,
|
|
24127
|
+
aiSessionsBySession: aiSummary.bySession.sessions,
|
|
24128
|
+
aiUsersBySession: aiSummary.bySession.users,
|
|
24129
|
+
paidAiSessionsBySession: aiSummary.paidBySession.sessions,
|
|
24130
|
+
paidAiUsersBySession: aiSummary.paidBySession.users,
|
|
24131
|
+
organicAiSessionsBySession: aiSummary.organicBySession.sessions,
|
|
24132
|
+
organicAiUsersBySession: aiSummary.organicBySession.users,
|
|
23938
24133
|
socialReferrals: socialReferrals.map((r) => ({
|
|
23939
24134
|
source: r.source,
|
|
23940
24135
|
medium: r.medium,
|
|
@@ -23946,16 +24141,24 @@ async function ga4Routes(app, opts) {
|
|
|
23946
24141
|
socialUsers: socialTotals?.users ?? 0,
|
|
23947
24142
|
channelBreakdown,
|
|
23948
24143
|
organicSharePct: total > 0 ? Math.round(totalOrganicSessions / total * 100) : 0,
|
|
23949
|
-
aiSharePct: total > 0 ? Math.round(
|
|
23950
|
-
aiSharePctBySession: total > 0 ? Math.round(
|
|
24144
|
+
aiSharePct: total > 0 ? Math.round(aiSummary.deduped.sessions / total * 100) : 0,
|
|
24145
|
+
aiSharePctBySession: total > 0 ? Math.round(aiSummary.bySession.sessions / total * 100) : 0,
|
|
24146
|
+
paidAiSharePct: total > 0 ? Math.round(aiSummary.paidDeduped.sessions / total * 100) : 0,
|
|
24147
|
+
paidAiSharePctBySession: total > 0 ? Math.round(aiSummary.paidBySession.sessions / total * 100) : 0,
|
|
24148
|
+
organicAiSharePct: total > 0 ? Math.round(aiSummary.organicDeduped.sessions / total * 100) : 0,
|
|
24149
|
+
organicAiSharePctBySession: total > 0 ? Math.round(aiSummary.organicBySession.sessions / total * 100) : 0,
|
|
23951
24150
|
directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
|
|
23952
24151
|
socialSharePct: total > 0 ? Math.round(socialSessions / total * 100) : 0,
|
|
23953
24152
|
otherSessions: channelBreakdown.other.sessions,
|
|
23954
24153
|
otherSharePct: channelBreakdown.other.sharePct,
|
|
23955
24154
|
otherSharePctDisplay: channelBreakdown.other.sharePctDisplay,
|
|
23956
24155
|
organicSharePctDisplay: formatSharePct(totalOrganicSessions, total),
|
|
23957
|
-
aiSharePctDisplay: formatSharePct(
|
|
23958
|
-
aiSharePctBySessionDisplay: formatSharePct(
|
|
24156
|
+
aiSharePctDisplay: formatSharePct(aiSummary.deduped.sessions, total),
|
|
24157
|
+
aiSharePctBySessionDisplay: formatSharePct(aiSummary.bySession.sessions, total),
|
|
24158
|
+
paidAiSharePctDisplay: formatSharePct(aiSummary.paidDeduped.sessions, total),
|
|
24159
|
+
paidAiSharePctBySessionDisplay: formatSharePct(aiSummary.paidBySession.sessions, total),
|
|
24160
|
+
organicAiSharePctDisplay: formatSharePct(aiSummary.organicDeduped.sessions, total),
|
|
24161
|
+
organicAiSharePctBySessionDisplay: formatSharePct(aiSummary.organicBySession.sessions, total),
|
|
23959
24162
|
directSharePctDisplay: formatSharePct(totalDirectSessions, total),
|
|
23960
24163
|
socialSharePctDisplay: formatSharePct(socialSessions, total),
|
|
23961
24164
|
lastSyncedAt: latestSync?.syncedAt ?? null,
|
|
@@ -23978,6 +24181,7 @@ async function ga4Routes(app, opts) {
|
|
|
23978
24181
|
date: gaAiReferrals.date,
|
|
23979
24182
|
source: gaAiReferrals.source,
|
|
23980
24183
|
medium: gaAiReferrals.medium,
|
|
24184
|
+
trafficClass: gaAiReferrals.trafficClass,
|
|
23981
24185
|
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
23982
24186
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23983
24187
|
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
@@ -23986,6 +24190,7 @@ async function ga4Routes(app, opts) {
|
|
|
23986
24190
|
gaAiReferrals.date,
|
|
23987
24191
|
gaAiReferrals.source,
|
|
23988
24192
|
gaAiReferrals.medium,
|
|
24193
|
+
gaAiReferrals.trafficClass,
|
|
23989
24194
|
gaAiReferrals.sourceDimension,
|
|
23990
24195
|
sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
23991
24196
|
).orderBy(gaAiReferrals.date).all();
|