@ainyc/canonry 1.24.0 → 1.24.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/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-BUzyFRxr.js"></script>
|
|
16
16
|
<link rel="stylesheet" crossorigin href="./assets/index-BidvmvWJ.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
@@ -763,6 +763,7 @@ __export(schema_exports, {
|
|
|
763
763
|
competitors: () => competitors,
|
|
764
764
|
gaConnections: () => gaConnections,
|
|
765
765
|
gaTrafficSnapshots: () => gaTrafficSnapshots,
|
|
766
|
+
gaTrafficSummaries: () => gaTrafficSummaries,
|
|
766
767
|
googleConnections: () => googleConnections,
|
|
767
768
|
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
768
769
|
gscSearchData: () => gscSearchData,
|
|
@@ -1027,6 +1028,18 @@ var gaTrafficSnapshots = sqliteTable("ga_traffic_snapshots", {
|
|
|
1027
1028
|
index("idx_ga_traffic_project_date").on(table.projectId, table.date),
|
|
1028
1029
|
index("idx_ga_traffic_page").on(table.landingPage)
|
|
1029
1030
|
]);
|
|
1031
|
+
var gaTrafficSummaries = sqliteTable("ga_traffic_summaries", {
|
|
1032
|
+
id: text("id").primaryKey(),
|
|
1033
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1034
|
+
periodStart: text("period_start").notNull(),
|
|
1035
|
+
periodEnd: text("period_end").notNull(),
|
|
1036
|
+
totalSessions: integer("total_sessions").notNull().default(0),
|
|
1037
|
+
totalOrganicSessions: integer("total_organic_sessions").notNull().default(0),
|
|
1038
|
+
totalUsers: integer("total_users").notNull().default(0),
|
|
1039
|
+
syncedAt: text("synced_at").notNull()
|
|
1040
|
+
}, (table) => [
|
|
1041
|
+
index("idx_ga_summary_project").on(table.projectId)
|
|
1042
|
+
]);
|
|
1030
1043
|
var usageCounters = sqliteTable("usage_counters", {
|
|
1031
1044
|
id: text("id").primaryKey(),
|
|
1032
1045
|
scope: text("scope").notNull(),
|
|
@@ -1328,7 +1341,19 @@ var MIGRATIONS = [
|
|
|
1328
1341
|
synced_at TEXT NOT NULL
|
|
1329
1342
|
)`,
|
|
1330
1343
|
`CREATE INDEX IF NOT EXISTS idx_ga_traffic_project_date ON ga_traffic_snapshots(project_id, date)`,
|
|
1331
|
-
`CREATE INDEX IF NOT EXISTS idx_ga_traffic_page ON ga_traffic_snapshots(landing_page)
|
|
1344
|
+
`CREATE INDEX IF NOT EXISTS idx_ga_traffic_page ON ga_traffic_snapshots(landing_page)`,
|
|
1345
|
+
// v14: GA4 aggregate summaries — stores true unique user count per sync period
|
|
1346
|
+
`CREATE TABLE IF NOT EXISTS ga_traffic_summaries (
|
|
1347
|
+
id TEXT PRIMARY KEY,
|
|
1348
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1349
|
+
period_start TEXT NOT NULL,
|
|
1350
|
+
period_end TEXT NOT NULL,
|
|
1351
|
+
total_sessions INTEGER NOT NULL DEFAULT 0,
|
|
1352
|
+
total_organic_sessions INTEGER NOT NULL DEFAULT 0,
|
|
1353
|
+
total_users INTEGER NOT NULL DEFAULT 0,
|
|
1354
|
+
synced_at TEXT NOT NULL
|
|
1355
|
+
)`,
|
|
1356
|
+
`CREATE INDEX IF NOT EXISTS idx_ga_summary_project ON ga_traffic_summaries(project_id)`
|
|
1332
1357
|
];
|
|
1333
1358
|
function migrate(db) {
|
|
1334
1359
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -6927,6 +6952,36 @@ async function runReport(accessToken, propertyId, request) {
|
|
|
6927
6952
|
}
|
|
6928
6953
|
return await res.json();
|
|
6929
6954
|
}
|
|
6955
|
+
async function batchRunReports(accessToken, propertyId, requests) {
|
|
6956
|
+
const url = `${GA4_DATA_API_BASE}/properties/${propertyId}:batchRunReports`;
|
|
6957
|
+
const res = await fetch(url, {
|
|
6958
|
+
method: "POST",
|
|
6959
|
+
headers: {
|
|
6960
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
6961
|
+
"Content-Type": "application/json"
|
|
6962
|
+
},
|
|
6963
|
+
body: JSON.stringify({ requests })
|
|
6964
|
+
});
|
|
6965
|
+
if (res.status === 401 || res.status === 403) {
|
|
6966
|
+
const body = await res.text().catch(() => "");
|
|
6967
|
+
ga4Log("error", "batch-report.auth-failed", { propertyId, httpStatus: res.status });
|
|
6968
|
+
throw new GA4ApiError(
|
|
6969
|
+
`GA4 API authentication failed \u2014 check service account permissions. ${body}`,
|
|
6970
|
+
res.status
|
|
6971
|
+
);
|
|
6972
|
+
}
|
|
6973
|
+
if (res.status === 429) {
|
|
6974
|
+
ga4Log("error", "batch-report.rate-limited", { propertyId });
|
|
6975
|
+
throw new GA4ApiError("GA4 API rate limit exceeded", 429);
|
|
6976
|
+
}
|
|
6977
|
+
if (!res.ok) {
|
|
6978
|
+
const body = await res.text();
|
|
6979
|
+
ga4Log("error", "batch-report.error", { propertyId, httpStatus: res.status });
|
|
6980
|
+
throw new GA4ApiError(`GA4 API error (${res.status}): ${body}`, res.status);
|
|
6981
|
+
}
|
|
6982
|
+
const data = await res.json();
|
|
6983
|
+
return data.reports;
|
|
6984
|
+
}
|
|
6930
6985
|
function formatDate(d) {
|
|
6931
6986
|
return d.toISOString().split("T")[0];
|
|
6932
6987
|
}
|
|
@@ -7017,6 +7072,45 @@ async function verifyConnection(clientEmail, privateKey, propertyId) {
|
|
|
7017
7072
|
});
|
|
7018
7073
|
return true;
|
|
7019
7074
|
}
|
|
7075
|
+
async function fetchAggregateSummary(accessToken, propertyId, days) {
|
|
7076
|
+
const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
|
|
7077
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
7078
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
7079
|
+
startDate.setDate(startDate.getDate() - syncDays);
|
|
7080
|
+
ga4Log("info", "fetch-aggregate.start", { propertyId, days: syncDays });
|
|
7081
|
+
const dateRange = { startDate: formatDate(startDate), endDate: formatDate(endDate) };
|
|
7082
|
+
const batchRes = await batchRunReports(accessToken, propertyId, [
|
|
7083
|
+
{
|
|
7084
|
+
dateRanges: [dateRange],
|
|
7085
|
+
dimensions: [],
|
|
7086
|
+
metrics: [{ name: "sessions" }, { name: "totalUsers" }],
|
|
7087
|
+
limit: 1
|
|
7088
|
+
},
|
|
7089
|
+
{
|
|
7090
|
+
dateRanges: [dateRange],
|
|
7091
|
+
dimensions: [],
|
|
7092
|
+
metrics: [{ name: "sessions" }],
|
|
7093
|
+
dimensionFilter: {
|
|
7094
|
+
filter: {
|
|
7095
|
+
fieldName: "sessionDefaultChannelGrouping",
|
|
7096
|
+
stringFilter: { matchType: "EXACT", value: "Organic Search" }
|
|
7097
|
+
}
|
|
7098
|
+
},
|
|
7099
|
+
limit: 1
|
|
7100
|
+
}
|
|
7101
|
+
]);
|
|
7102
|
+
const totalRow = batchRes[0]?.rows?.[0];
|
|
7103
|
+
const organicRow = batchRes[1]?.rows?.[0];
|
|
7104
|
+
const summary = {
|
|
7105
|
+
periodStart: formatDate(startDate),
|
|
7106
|
+
periodEnd: formatDate(endDate),
|
|
7107
|
+
totalSessions: parseInt(totalRow?.metricValues[0]?.value ?? "0", 10) || 0,
|
|
7108
|
+
totalUsers: parseInt(totalRow?.metricValues[1]?.value ?? "0", 10) || 0,
|
|
7109
|
+
totalOrganicSessions: parseInt(organicRow?.metricValues[0]?.value ?? "0", 10) || 0
|
|
7110
|
+
};
|
|
7111
|
+
ga4Log("info", "fetch-aggregate.done", { propertyId, ...summary });
|
|
7112
|
+
return summary;
|
|
7113
|
+
}
|
|
7020
7114
|
|
|
7021
7115
|
// ../api-routes/src/ga.ts
|
|
7022
7116
|
function gaLog(level, action, ctx) {
|
|
@@ -7101,6 +7195,7 @@ async function ga4Routes(app, opts) {
|
|
|
7101
7195
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
7102
7196
|
}
|
|
7103
7197
|
app.db.delete(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).run();
|
|
7198
|
+
app.db.delete(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).run();
|
|
7104
7199
|
store.deleteConnection(project.name);
|
|
7105
7200
|
writeAuditLog(app.db, {
|
|
7106
7201
|
projectId: project.id,
|
|
@@ -7149,19 +7244,24 @@ async function ga4Routes(app, opts) {
|
|
|
7149
7244
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
7150
7245
|
}
|
|
7151
7246
|
let rows;
|
|
7247
|
+
let summary;
|
|
7152
7248
|
try {
|
|
7153
|
-
|
|
7249
|
+
;
|
|
7250
|
+
[rows, summary] = await Promise.all([
|
|
7251
|
+
fetchTrafficByLandingPage(accessToken, conn.propertyId, days),
|
|
7252
|
+
fetchAggregateSummary(accessToken, conn.propertyId, days)
|
|
7253
|
+
]);
|
|
7154
7254
|
} catch (e) {
|
|
7155
7255
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7156
7256
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, error: msg });
|
|
7157
7257
|
throw e;
|
|
7158
7258
|
}
|
|
7159
7259
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7260
|
+
app.db.transaction((tx) => {
|
|
7261
|
+
if (rows.length > 0) {
|
|
7262
|
+
const dates = rows.map((r) => r.date);
|
|
7263
|
+
const minDate = dates.reduce((a, b) => a < b ? a : b);
|
|
7264
|
+
const maxDate = dates.reduce((a, b) => a > b ? a : b);
|
|
7165
7265
|
tx.delete(gaTrafficSnapshots).where(
|
|
7166
7266
|
and6(
|
|
7167
7267
|
eq16(gaTrafficSnapshots.projectId, project.id),
|
|
@@ -7181,9 +7281,20 @@ async function ga4Routes(app, opts) {
|
|
|
7181
7281
|
syncedAt: now
|
|
7182
7282
|
}).run();
|
|
7183
7283
|
}
|
|
7184
|
-
}
|
|
7185
|
-
|
|
7186
|
-
|
|
7284
|
+
}
|
|
7285
|
+
tx.delete(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).run();
|
|
7286
|
+
tx.insert(gaTrafficSummaries).values({
|
|
7287
|
+
id: crypto16.randomUUID(),
|
|
7288
|
+
projectId: project.id,
|
|
7289
|
+
periodStart: summary.periodStart,
|
|
7290
|
+
periodEnd: summary.periodEnd,
|
|
7291
|
+
totalSessions: summary.totalSessions,
|
|
7292
|
+
totalOrganicSessions: summary.totalOrganicSessions,
|
|
7293
|
+
totalUsers: summary.totalUsers,
|
|
7294
|
+
syncedAt: now
|
|
7295
|
+
}).run();
|
|
7296
|
+
});
|
|
7297
|
+
gaLog("info", "sync.complete", { projectId: project.id, rowCount: rows.length, days, totalUsers: summary.totalUsers });
|
|
7187
7298
|
return {
|
|
7188
7299
|
synced: true,
|
|
7189
7300
|
rowCount: rows.length,
|
|
@@ -7201,11 +7312,11 @@ async function ga4Routes(app, opts) {
|
|
|
7201
7312
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
7202
7313
|
}
|
|
7203
7314
|
const limit = Math.max(1, Math.min(parseInt(request.query.limit ?? "50", 10) || 50, 500));
|
|
7204
|
-
const
|
|
7205
|
-
totalSessions:
|
|
7206
|
-
totalOrganicSessions:
|
|
7207
|
-
totalUsers:
|
|
7208
|
-
}).from(
|
|
7315
|
+
const summary = app.db.select({
|
|
7316
|
+
totalSessions: gaTrafficSummaries.totalSessions,
|
|
7317
|
+
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
7318
|
+
totalUsers: gaTrafficSummaries.totalUsers
|
|
7319
|
+
}).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).get();
|
|
7209
7320
|
const rows = app.db.select({
|
|
7210
7321
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
7211
7322
|
sessions: sql3`SUM(${gaTrafficSnapshots.sessions})`,
|
|
@@ -7214,9 +7325,9 @@ async function ga4Routes(app, opts) {
|
|
|
7214
7325
|
}).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql3`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
7215
7326
|
const latestSync = app.db.select({ syncedAt: gaTrafficSnapshots.syncedAt }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).orderBy(desc6(gaTrafficSnapshots.syncedAt)).limit(1).get();
|
|
7216
7327
|
return {
|
|
7217
|
-
totalSessions:
|
|
7218
|
-
totalOrganicSessions:
|
|
7219
|
-
totalUsers:
|
|
7328
|
+
totalSessions: summary?.totalSessions ?? 0,
|
|
7329
|
+
totalOrganicSessions: summary?.totalOrganicSessions ?? 0,
|
|
7330
|
+
totalUsers: summary?.totalUsers ?? 0,
|
|
7220
7331
|
topPages: rows.map((r) => ({
|
|
7221
7332
|
landingPage: r.landingPage,
|
|
7222
7333
|
sessions: r.sessions ?? 0,
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
|
|
6
6
|
"license": "FSL-1.1-ALv2",
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"@ainyc/canonry-api-routes": "0.0.0",
|
|
56
56
|
"@ainyc/canonry-config": "0.0.0",
|
|
57
57
|
"@ainyc/canonry-contracts": "0.0.0",
|
|
58
|
-
"@ainyc/canonry-db": "0.0.0",
|
|
59
58
|
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
60
|
-
"@ainyc/canonry-
|
|
59
|
+
"@ainyc/canonry-db": "0.0.0",
|
|
61
60
|
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
62
61
|
"@ainyc/canonry-provider-local": "0.0.0",
|
|
63
62
|
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
64
|
-
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
65
63
|
"@ainyc/canonry-integration-google": "0.0.0",
|
|
64
|
+
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
65
|
+
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
66
66
|
"@ainyc/canonry-provider-perplexity": "0.0.0"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|