@ainyc/canonry 2.14.0 → 2.14.2
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-BwFUCV6e.js → index-D1v6Q-fS.js} +36 -36
- package/assets/index.html +1 -1
- package/dist/{chunk-RNMMN2WI.js → chunk-7VWSR5F6.js} +98 -0
- package/dist/{chunk-LD7Y4K4G.js → chunk-CILBPOHB.js} +48 -6
- package/dist/{chunk-UM6RDSRJ.js → chunk-NEDRCOOL.js} +33 -1
- package/dist/cli.js +103 -8
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-54F3NGPM.js → intelligence-service-6S5YKANX.js} +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +7 -7
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-D1v6Q-fS.js"></script>
|
|
16
16
|
<link rel="stylesheet" crossorigin href="./assets/index-U2SLimrz.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
@@ -1298,11 +1298,15 @@ var ga4SocialReferralDtoSchema = z12.object({
|
|
|
1298
1298
|
var ga4TrafficSummaryDtoSchema = z12.object({
|
|
1299
1299
|
totalSessions: z12.number(),
|
|
1300
1300
|
totalOrganicSessions: z12.number(),
|
|
1301
|
+
/** Direct-channel sessions (sessions with no source — bookmarks, typed URLs, AI-driven traffic with stripped referrer). 0 for legacy rows from before the column was added. */
|
|
1302
|
+
totalDirectSessions: z12.number(),
|
|
1301
1303
|
totalUsers: z12.number(),
|
|
1302
1304
|
topPages: z12.array(z12.object({
|
|
1303
1305
|
landingPage: z12.string(),
|
|
1304
1306
|
sessions: z12.number(),
|
|
1305
1307
|
organicSessions: z12.number(),
|
|
1308
|
+
/** Per-page Direct-channel sessions. 0 for legacy rows. */
|
|
1309
|
+
directSessions: z12.number(),
|
|
1306
1310
|
users: z12.number()
|
|
1307
1311
|
})),
|
|
1308
1312
|
aiReferrals: z12.array(ga4AiReferralDtoSchema),
|
|
@@ -1319,6 +1323,8 @@ var ga4TrafficSummaryDtoSchema = z12.object({
|
|
|
1319
1323
|
organicSharePct: z12.number(),
|
|
1320
1324
|
/** Deduped AI sessions as a percentage of total sessions (0–100, rounded). */
|
|
1321
1325
|
aiSharePct: z12.number(),
|
|
1326
|
+
/** Direct-channel sessions as a percentage of total sessions (0–100, rounded). */
|
|
1327
|
+
directSharePct: z12.number(),
|
|
1322
1328
|
/** Social sessions as a percentage of total sessions (0–100, rounded). */
|
|
1323
1329
|
socialSharePct: z12.number(),
|
|
1324
1330
|
lastSyncedAt: z12.string().nullable()
|
|
@@ -1790,6 +1796,97 @@ function summarizeCheckResults(results) {
|
|
|
1790
1796
|
return summary;
|
|
1791
1797
|
}
|
|
1792
1798
|
|
|
1799
|
+
// ../contracts/src/url-normalize.ts
|
|
1800
|
+
var STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
1801
|
+
// Click identifiers
|
|
1802
|
+
"fbclid",
|
|
1803
|
+
"gclid",
|
|
1804
|
+
"msclkid",
|
|
1805
|
+
"ttclid",
|
|
1806
|
+
"li_fat_id",
|
|
1807
|
+
"igshid",
|
|
1808
|
+
"yclid",
|
|
1809
|
+
"dclid",
|
|
1810
|
+
"gbraid",
|
|
1811
|
+
"wbraid",
|
|
1812
|
+
// Mailchimp
|
|
1813
|
+
"mc_cid",
|
|
1814
|
+
"mc_eid",
|
|
1815
|
+
// Google Analytics linkers
|
|
1816
|
+
"_ga",
|
|
1817
|
+
"_gl",
|
|
1818
|
+
// Google Tag Manager debug
|
|
1819
|
+
"gtm_latency",
|
|
1820
|
+
"gtm_debug"
|
|
1821
|
+
]);
|
|
1822
|
+
function shouldStrip(key) {
|
|
1823
|
+
if (STRIP_KEYS.has(key)) return true;
|
|
1824
|
+
if (key.startsWith("utm_")) return true;
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
function parseQuery(query) {
|
|
1828
|
+
if (query === "") return [];
|
|
1829
|
+
return query.split("&").map((pair) => {
|
|
1830
|
+
const eq = pair.indexOf("=");
|
|
1831
|
+
if (eq === -1) return { key: pair, value: null };
|
|
1832
|
+
return { key: pair.slice(0, eq), value: pair.slice(eq + 1) };
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
function encodeQuery(pairs) {
|
|
1836
|
+
return pairs.map((p) => p.value === null ? p.key : `${p.key}=${p.value}`).join("&");
|
|
1837
|
+
}
|
|
1838
|
+
function collapseRootIndex(path2) {
|
|
1839
|
+
if (path2 === "/index.html" || path2 === "/index.php") return "/";
|
|
1840
|
+
return path2;
|
|
1841
|
+
}
|
|
1842
|
+
function dropTrailingSlash(path2) {
|
|
1843
|
+
if (path2.length > 1 && path2.endsWith("/")) {
|
|
1844
|
+
return path2.replace(/\/+$/, "");
|
|
1845
|
+
}
|
|
1846
|
+
return path2;
|
|
1847
|
+
}
|
|
1848
|
+
function normalizeUrlPath(input) {
|
|
1849
|
+
if (input == null) return null;
|
|
1850
|
+
const trimmed = input.trim();
|
|
1851
|
+
if (trimmed === "") return null;
|
|
1852
|
+
if (trimmed === "(not set)") return null;
|
|
1853
|
+
let pathPart;
|
|
1854
|
+
let queryPart;
|
|
1855
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
1856
|
+
let url;
|
|
1857
|
+
try {
|
|
1858
|
+
url = new URL(trimmed);
|
|
1859
|
+
} catch {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
pathPart = url.pathname || "/";
|
|
1863
|
+
queryPart = url.search.startsWith("?") ? url.search.slice(1) : url.search;
|
|
1864
|
+
} else {
|
|
1865
|
+
let raw = trimmed;
|
|
1866
|
+
const hashIdx = raw.indexOf("#");
|
|
1867
|
+
if (hashIdx !== -1) raw = raw.slice(0, hashIdx);
|
|
1868
|
+
const qIdx = raw.indexOf("?");
|
|
1869
|
+
if (qIdx === -1) {
|
|
1870
|
+
pathPart = raw;
|
|
1871
|
+
queryPart = "";
|
|
1872
|
+
} else {
|
|
1873
|
+
pathPart = raw.slice(0, qIdx);
|
|
1874
|
+
queryPart = raw.slice(qIdx + 1);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (pathPart === "") pathPart = "/";
|
|
1878
|
+
pathPart = collapseRootIndex(pathPart);
|
|
1879
|
+
pathPart = dropTrailingSlash(pathPart);
|
|
1880
|
+
const pairs = parseQuery(queryPart).filter((p) => !shouldStrip(p.key));
|
|
1881
|
+
pairs.sort((a, b) => {
|
|
1882
|
+
if (a.key < b.key) return -1;
|
|
1883
|
+
if (a.key > b.key) return 1;
|
|
1884
|
+
return 0;
|
|
1885
|
+
});
|
|
1886
|
+
if (pairs.length === 0) return pathPart;
|
|
1887
|
+
return `${pathPart}?${encodeQuery(pairs)}`;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1793
1890
|
// src/client.ts
|
|
1794
1891
|
function createApiClient() {
|
|
1795
1892
|
const config = loadConfig();
|
|
@@ -2522,6 +2619,7 @@ export {
|
|
|
2522
2619
|
CheckScopes,
|
|
2523
2620
|
CheckCategories,
|
|
2524
2621
|
summarizeCheckResults,
|
|
2622
|
+
normalizeUrlPath,
|
|
2525
2623
|
createApiClient,
|
|
2526
2624
|
ApiClient
|
|
2527
2625
|
};
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
locationContextSchema,
|
|
40
40
|
missingDependency,
|
|
41
41
|
normalizeProjectDomain,
|
|
42
|
+
normalizeUrlPath,
|
|
42
43
|
notFound,
|
|
43
44
|
notImplemented,
|
|
44
45
|
parseRunError,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
visibilityStateFromAnswerMentioned,
|
|
60
61
|
windowCutoff,
|
|
61
62
|
wordpressEnvSchema
|
|
62
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-7VWSR5F6.js";
|
|
63
64
|
import {
|
|
64
65
|
IntelligenceService,
|
|
65
66
|
agentMemory,
|
|
@@ -97,7 +98,7 @@ import {
|
|
|
97
98
|
runs,
|
|
98
99
|
schedules,
|
|
99
100
|
usageCounters
|
|
100
|
-
} from "./chunk-
|
|
101
|
+
} from "./chunk-NEDRCOOL.js";
|
|
101
102
|
|
|
102
103
|
// src/telemetry.ts
|
|
103
104
|
import crypto from "crypto";
|
|
@@ -6661,6 +6662,8 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6661
6662
|
sessions: parseInt(row.metricValues[0].value, 10) || 0,
|
|
6662
6663
|
organicSessions: 0,
|
|
6663
6664
|
// populated by organic-only pass below
|
|
6665
|
+
directSessions: 0,
|
|
6666
|
+
// populated by direct-only pass below
|
|
6664
6667
|
users: parseInt(row.metricValues[1].value, 10) || 0
|
|
6665
6668
|
}));
|
|
6666
6669
|
rows.push(...pageRows);
|
|
@@ -6696,9 +6699,38 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
|
|
|
6696
6699
|
organicOffset += (organicResponse.rows ?? []).length;
|
|
6697
6700
|
if ((organicResponse.rows ?? []).length < 1e4 || organicOffset >= total) break;
|
|
6698
6701
|
}
|
|
6702
|
+
const directMap = /* @__PURE__ */ new Map();
|
|
6703
|
+
let directOffset = 0;
|
|
6704
|
+
let directPageCount = 0;
|
|
6705
|
+
while (directPageCount < GA4_MAX_PAGES) {
|
|
6706
|
+
directPageCount++;
|
|
6707
|
+
const directRequest = {
|
|
6708
|
+
dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
|
|
6709
|
+
dimensions: [{ name: "date" }, { name: "landingPagePlusQueryString" }],
|
|
6710
|
+
metrics: [{ name: "sessions" }],
|
|
6711
|
+
dimensionFilter: {
|
|
6712
|
+
filter: {
|
|
6713
|
+
fieldName: "sessionDefaultChannelGrouping",
|
|
6714
|
+
stringFilter: { matchType: "EXACT", value: "Direct" }
|
|
6715
|
+
}
|
|
6716
|
+
},
|
|
6717
|
+
limit: 1e4,
|
|
6718
|
+
offset: directOffset
|
|
6719
|
+
};
|
|
6720
|
+
const directResponse = await runReport(accessToken, propertyId, directRequest);
|
|
6721
|
+
if (!directResponse) break;
|
|
6722
|
+
for (const row of directResponse.rows ?? []) {
|
|
6723
|
+
const key = `${row.dimensionValues[0].value}::${row.dimensionValues[1].value}`;
|
|
6724
|
+
directMap.set(key, parseInt(row.metricValues[0].value, 10) || 0);
|
|
6725
|
+
}
|
|
6726
|
+
const total = directResponse.rowCount ?? 0;
|
|
6727
|
+
directOffset += (directResponse.rows ?? []).length;
|
|
6728
|
+
if ((directResponse.rows ?? []).length < 1e4 || directOffset >= total) break;
|
|
6729
|
+
}
|
|
6699
6730
|
for (const row of rows) {
|
|
6700
6731
|
const key = `${row.date}::${row.landingPage}`;
|
|
6701
6732
|
row.organicSessions = organicMap.get(key) ?? 0;
|
|
6733
|
+
row.directSessions = directMap.get(key) ?? 0;
|
|
6702
6734
|
}
|
|
6703
6735
|
for (const row of rows) {
|
|
6704
6736
|
if (row.date.length === 8 && !row.date.includes("-")) {
|
|
@@ -8661,8 +8693,10 @@ async function ga4Routes(app, opts) {
|
|
|
8661
8693
|
projectId: project.id,
|
|
8662
8694
|
date: row.date,
|
|
8663
8695
|
landingPage: row.landingPage,
|
|
8696
|
+
landingPageNormalized: normalizeUrlPath(row.landingPage),
|
|
8664
8697
|
sessions: row.sessions,
|
|
8665
8698
|
organicSessions: row.organicSessions,
|
|
8699
|
+
directSessions: row.directSessions,
|
|
8666
8700
|
users: row.users,
|
|
8667
8701
|
syncedAt: now,
|
|
8668
8702
|
syncRunId: runId
|
|
@@ -8780,16 +8814,20 @@ async function ga4Routes(app, opts) {
|
|
|
8780
8814
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
8781
8815
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
8782
8816
|
}).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
|
|
8817
|
+
const directTotalRow = app.db.select({
|
|
8818
|
+
totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
8819
|
+
}).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get();
|
|
8783
8820
|
const summaryMeta = app.db.select({
|
|
8784
8821
|
periodStart: gaTrafficSummaries.periodStart,
|
|
8785
8822
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
8786
8823
|
}).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
|
|
8787
8824
|
const rows = app.db.select({
|
|
8788
|
-
landingPage: gaTrafficSnapshots.landingPage
|
|
8825
|
+
landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
8789
8826
|
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8790
8827
|
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8828
|
+
directSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
8791
8829
|
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
8792
|
-
}).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8830
|
+
}).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8793
8831
|
const aiReferrals = app.db.select({
|
|
8794
8832
|
source: gaAiReferrals.source,
|
|
8795
8833
|
medium: gaAiReferrals.medium,
|
|
@@ -8823,14 +8861,17 @@ async function ga4Routes(app, opts) {
|
|
|
8823
8861
|
}).from(gaSocialReferrals).where(and8(...socialConditions)).get();
|
|
8824
8862
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8825
8863
|
const total = summaryRow?.totalSessions ?? 0;
|
|
8864
|
+
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
8826
8865
|
return {
|
|
8827
8866
|
totalSessions: total,
|
|
8828
8867
|
totalOrganicSessions: summaryRow?.totalOrganicSessions ?? 0,
|
|
8868
|
+
totalDirectSessions,
|
|
8829
8869
|
totalUsers: summaryRow?.totalUsers ?? 0,
|
|
8830
8870
|
topPages: rows.map((r) => ({
|
|
8831
8871
|
landingPage: r.landingPage,
|
|
8832
8872
|
sessions: r.sessions ?? 0,
|
|
8833
8873
|
organicSessions: r.organicSessions ?? 0,
|
|
8874
|
+
directSessions: r.directSessions ?? 0,
|
|
8834
8875
|
users: r.users ?? 0
|
|
8835
8876
|
})),
|
|
8836
8877
|
aiReferrals: aiReferrals.map((r) => ({
|
|
@@ -8853,6 +8894,7 @@ async function ga4Routes(app, opts) {
|
|
|
8853
8894
|
socialUsers: socialTotals?.users ?? 0,
|
|
8854
8895
|
organicSharePct: total > 0 ? Math.round((summaryRow?.totalOrganicSessions ?? 0) / total * 100) : 0,
|
|
8855
8896
|
aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
|
|
8897
|
+
directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
|
|
8856
8898
|
socialSharePct: total > 0 ? Math.round((socialTotals?.sessions ?? 0) / total * 100) : 0,
|
|
8857
8899
|
lastSyncedAt: latestSync?.syncedAt ?? null,
|
|
8858
8900
|
periodStart: (() => {
|
|
@@ -9046,11 +9088,11 @@ async function ga4Routes(app, opts) {
|
|
|
9046
9088
|
const project = resolveProject(app.db, request.params.name);
|
|
9047
9089
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
9048
9090
|
const trafficPages = app.db.select({
|
|
9049
|
-
landingPage: gaTrafficSnapshots.landingPage
|
|
9091
|
+
landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
9050
9092
|
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
9051
9093
|
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
9052
9094
|
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
9053
|
-
}).from(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
9095
|
+
}).from(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
9054
9096
|
return {
|
|
9055
9097
|
pages: trafficPages.map((r) => ({
|
|
9056
9098
|
landingPage: r.landingPage,
|
|
@@ -310,14 +310,30 @@ var gaTrafficSnapshots = sqliteTable("ga_traffic_snapshots", {
|
|
|
310
310
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
311
311
|
date: text("date").notNull(),
|
|
312
312
|
landingPage: text("landing_page").notNull(),
|
|
313
|
+
/**
|
|
314
|
+
* Canonicalized form of `landingPage` produced by `normalizeUrlPath()` in
|
|
315
|
+
* `@ainyc/canonry-contracts`. Nullable so existing rows survive migration;
|
|
316
|
+
* new GA4 sync writes populate it. Per-page aggregations should
|
|
317
|
+
* `GROUP BY COALESCE(landing_page_normalized, landing_page)` so
|
|
318
|
+
* partially-backfilled state still aggregates correctly.
|
|
319
|
+
*/
|
|
320
|
+
landingPageNormalized: text("landing_page_normalized"),
|
|
313
321
|
sessions: integer("sessions").notNull().default(0),
|
|
314
322
|
organicSessions: integer("organic_sessions").notNull().default(0),
|
|
323
|
+
/**
|
|
324
|
+
* Per-page Direct channel sessions. Nullable so existing rows survive
|
|
325
|
+
* the migration; new GA4 sync writes populate it. Distinct from
|
|
326
|
+
* `sessions - organicSessions` because that residual lumps Direct
|
|
327
|
+
* together with social, referral, paid, and email.
|
|
328
|
+
*/
|
|
329
|
+
directSessions: integer("direct_sessions"),
|
|
315
330
|
users: integer("users").notNull().default(0),
|
|
316
331
|
syncedAt: text("synced_at").notNull(),
|
|
317
332
|
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" })
|
|
318
333
|
}, (table) => [
|
|
319
334
|
index("idx_ga_traffic_project_date").on(table.projectId, table.date),
|
|
320
335
|
index("idx_ga_traffic_page").on(table.landingPage),
|
|
336
|
+
index("idx_ga_traffic_page_normalized").on(table.projectId, table.date, table.landingPageNormalized),
|
|
321
337
|
index("idx_ga_traffic_run").on(table.syncRunId)
|
|
322
338
|
]);
|
|
323
339
|
var gaAiReferrals = sqliteTable("ga_ai_referrals", {
|
|
@@ -1049,7 +1065,23 @@ var MIGRATIONS = [
|
|
|
1049
1065
|
WHEN discovery_date IS NOT NULL THEN 0
|
|
1050
1066
|
ELSE NULL
|
|
1051
1067
|
END
|
|
1052
|
-
WHERE created_at < '2026-04-22T00:00:00Z'
|
|
1068
|
+
WHERE created_at < '2026-04-22T00:00:00Z'`,
|
|
1069
|
+
// v44: Canonicalized landing-page column for ga_traffic_snapshots.
|
|
1070
|
+
// Populated by GA4 sync via normalizeUrlPath() in
|
|
1071
|
+
// @ainyc/canonry-contracts. Nullable; existing rows are filled in by
|
|
1072
|
+
// `canonry backfill normalized-paths`. Read queries should
|
|
1073
|
+
// `GROUP BY COALESCE(landing_page_normalized, landing_page)` so
|
|
1074
|
+
// partially-backfilled state still aggregates correctly.
|
|
1075
|
+
// See plans/ai-attribution-research.md "Step 1 — data hygiene".
|
|
1076
|
+
`ALTER TABLE ga_traffic_snapshots ADD COLUMN landing_page_normalized TEXT`,
|
|
1077
|
+
`CREATE INDEX IF NOT EXISTS idx_ga_traffic_page_normalized
|
|
1078
|
+
ON ga_traffic_snapshots(project_id, date, landing_page_normalized)`,
|
|
1079
|
+
// v45: Per-page Direct channel sessions on ga_traffic_snapshots. Nullable
|
|
1080
|
+
// so existing rows survive; populated by the GA4 sync writer in a
|
|
1081
|
+
// separate commit. Unblocks an honest channel breakdown for the project
|
|
1082
|
+
// dashboard (organic / social / direct / known-AI) — see
|
|
1083
|
+
// plans/ai-attribution-research.md scope A.
|
|
1084
|
+
`ALTER TABLE ga_traffic_snapshots ADD COLUMN direct_sessions INTEGER`
|
|
1053
1085
|
];
|
|
1054
1086
|
function isDuplicateColumnError(err) {
|
|
1055
1087
|
if (!(err instanceof Error)) return false;
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
setGoogleAuthConfig,
|
|
18
18
|
showFirstRunNotice,
|
|
19
19
|
trackEvent
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-CILBPOHB.js";
|
|
21
21
|
import {
|
|
22
22
|
CcReleaseSyncStatuses,
|
|
23
23
|
CheckScopes,
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
getConfigPath,
|
|
38
38
|
isEndpointMissing,
|
|
39
39
|
loadConfig,
|
|
40
|
+
normalizeUrlPath,
|
|
40
41
|
notificationEventSchema,
|
|
41
42
|
printCliError,
|
|
42
43
|
providerQuotaPolicySchema,
|
|
@@ -44,17 +45,18 @@ import {
|
|
|
44
45
|
saveConfig,
|
|
45
46
|
saveConfigPatch,
|
|
46
47
|
usageError
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-7VWSR5F6.js";
|
|
48
49
|
import {
|
|
49
50
|
apiKeys,
|
|
50
51
|
competitors,
|
|
51
52
|
createClient,
|
|
53
|
+
gaTrafficSnapshots,
|
|
52
54
|
migrate,
|
|
53
55
|
parseJsonColumn,
|
|
54
56
|
projects,
|
|
55
57
|
querySnapshots,
|
|
56
58
|
runs
|
|
57
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-NEDRCOOL.js";
|
|
58
60
|
import "./chunk-MLKGABMK.js";
|
|
59
61
|
|
|
60
62
|
// src/cli.ts
|
|
@@ -160,7 +162,7 @@ Usage: ${spec.usage}`, {
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
// src/commands/backfill.ts
|
|
163
|
-
import { and, eq, inArray } from "drizzle-orm";
|
|
165
|
+
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
164
166
|
var SNAPSHOT_BATCH_SIZE = 500;
|
|
165
167
|
async function backfillAnswerVisibilityCommand(opts) {
|
|
166
168
|
const config = loadConfig();
|
|
@@ -301,8 +303,75 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
301
303
|
console.log(` Reparsed: ${reparsed}`);
|
|
302
304
|
console.log(` Errors: ${providerErrors}`);
|
|
303
305
|
}
|
|
306
|
+
function backfillNormalizedPaths(db, opts) {
|
|
307
|
+
const baseConditions = [isNull(gaTrafficSnapshots.landingPageNormalized)];
|
|
308
|
+
if (opts?.projectId) {
|
|
309
|
+
baseConditions.push(eq(gaTrafficSnapshots.projectId, opts.projectId));
|
|
310
|
+
}
|
|
311
|
+
const rows = db.select({
|
|
312
|
+
id: gaTrafficSnapshots.id,
|
|
313
|
+
landingPage: gaTrafficSnapshots.landingPage
|
|
314
|
+
}).from(gaTrafficSnapshots).where(and(...baseConditions)).all();
|
|
315
|
+
let updated = 0;
|
|
316
|
+
let unchanged = 0;
|
|
317
|
+
if (rows.length > 0) {
|
|
318
|
+
db.transaction((tx) => {
|
|
319
|
+
for (const row of rows) {
|
|
320
|
+
const next = normalizeUrlPath(row.landingPage);
|
|
321
|
+
if (next === null) {
|
|
322
|
+
unchanged++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq(gaTrafficSnapshots.id, row.id)).run();
|
|
326
|
+
updated++;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return { examined: rows.length, updated, unchanged };
|
|
331
|
+
}
|
|
332
|
+
async function backfillNormalizedPathsCommand(opts) {
|
|
333
|
+
const config = loadConfig();
|
|
334
|
+
const db = createClient(config.database);
|
|
335
|
+
migrate(db);
|
|
336
|
+
const projectFilter = opts?.project?.trim();
|
|
337
|
+
let projectId;
|
|
338
|
+
if (projectFilter) {
|
|
339
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectFilter)).get();
|
|
340
|
+
if (!project) {
|
|
341
|
+
const result2 = {
|
|
342
|
+
project: projectFilter,
|
|
343
|
+
examined: 0,
|
|
344
|
+
updated: 0,
|
|
345
|
+
unchanged: 0
|
|
346
|
+
};
|
|
347
|
+
if (opts?.format === "json") {
|
|
348
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
projectId = project.id;
|
|
355
|
+
}
|
|
356
|
+
const { examined, updated, unchanged } = backfillNormalizedPaths(db, { projectId });
|
|
357
|
+
const result = {
|
|
358
|
+
project: projectFilter ?? null,
|
|
359
|
+
examined,
|
|
360
|
+
updated,
|
|
361
|
+
unchanged
|
|
362
|
+
};
|
|
363
|
+
if (opts?.format === "json") {
|
|
364
|
+
console.log(JSON.stringify(result, null, 2));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
console.log("Normalized-path backfill complete.\n");
|
|
368
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
369
|
+
console.log(` Examined: ${examined}`);
|
|
370
|
+
console.log(` Updated: ${updated}`);
|
|
371
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
372
|
+
}
|
|
304
373
|
async function backfillInsightsCommand(project, opts) {
|
|
305
|
-
const { IntelligenceService } = await import("./intelligence-service-
|
|
374
|
+
const { IntelligenceService } = await import("./intelligence-service-6S5YKANX.js");
|
|
306
375
|
const config = loadConfig();
|
|
307
376
|
const db = createClient(config.database);
|
|
308
377
|
migrate(db);
|
|
@@ -498,14 +567,28 @@ var BACKFILL_CLI_COMMANDS = [
|
|
|
498
567
|
});
|
|
499
568
|
}
|
|
500
569
|
},
|
|
570
|
+
{
|
|
571
|
+
path: ["backfill", "normalized-paths"],
|
|
572
|
+
usage: "canonry backfill normalized-paths [--project <name>] [--format json]",
|
|
573
|
+
options: {
|
|
574
|
+
project: stringOption()
|
|
575
|
+
},
|
|
576
|
+
allowPositionals: false,
|
|
577
|
+
run: async (input) => {
|
|
578
|
+
await backfillNormalizedPathsCommand({
|
|
579
|
+
project: getString(input.values, "project"),
|
|
580
|
+
format: input.format
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
},
|
|
501
584
|
{
|
|
502
585
|
path: ["backfill"],
|
|
503
|
-
usage: "canonry backfill <answer-visibility|insights> [options]",
|
|
586
|
+
usage: "canonry backfill <answer-visibility|insights|normalized-paths> [options]",
|
|
504
587
|
run: async (input) => {
|
|
505
588
|
unknownSubcommand(input.positionals[0], {
|
|
506
589
|
command: "backfill",
|
|
507
|
-
usage: "canonry backfill <answer-visibility|insights> [options]",
|
|
508
|
-
available: ["answer-visibility", "insights"]
|
|
590
|
+
usage: "canonry backfill <answer-visibility|insights|normalized-paths> [options]",
|
|
591
|
+
available: ["answer-visibility", "insights", "normalized-paths"]
|
|
509
592
|
});
|
|
510
593
|
}
|
|
511
594
|
}
|
|
@@ -7072,6 +7155,18 @@ async function serveCommand(format = "text") {
|
|
|
7072
7155
|
config.port = port;
|
|
7073
7156
|
const db = createClient(config.database);
|
|
7074
7157
|
migrate(db);
|
|
7158
|
+
try {
|
|
7159
|
+
const result = backfillNormalizedPaths(db);
|
|
7160
|
+
if (result.updated > 0 && format === "text") {
|
|
7161
|
+
console.log(
|
|
7162
|
+
`Migrated ${result.updated} GA landing-page row${result.updated === 1 ? "" : "s"} to canonical form.`
|
|
7163
|
+
);
|
|
7164
|
+
}
|
|
7165
|
+
} catch (err) {
|
|
7166
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7167
|
+
process.stderr.write(`warning: normalized-path backfill skipped: ${msg}
|
|
7168
|
+
`);
|
|
7169
|
+
}
|
|
7075
7170
|
const app = await createServer({ config, db });
|
|
7076
7171
|
let shuttingDown = false;
|
|
7077
7172
|
const shutdown = (signal) => {
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CILBPOHB.js";
|
|
4
4
|
import {
|
|
5
5
|
loadConfig
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-7VWSR5F6.js";
|
|
7
|
+
import "./chunk-NEDRCOOL.js";
|
|
8
8
|
import "./chunk-MLKGABMK.js";
|
|
9
9
|
export {
|
|
10
10
|
createServer,
|
package/dist/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
|
|
6
6
|
"license": "FSL-1.1-ALv2",
|
|
@@ -60,20 +60,20 @@
|
|
|
60
60
|
"tsup": "^8.5.1",
|
|
61
61
|
"tsx": "^4.19.0",
|
|
62
62
|
"@ainyc/canonry-config": "0.0.0",
|
|
63
|
-
"@ainyc/canonry-api-routes": "0.0.0",
|
|
64
63
|
"@ainyc/canonry-contracts": "0.0.0",
|
|
65
64
|
"@ainyc/canonry-db": "0.0.0",
|
|
65
|
+
"@ainyc/canonry-api-routes": "0.0.0",
|
|
66
66
|
"@ainyc/canonry-intelligence": "0.0.0",
|
|
67
|
-
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
68
67
|
"@ainyc/canonry-integration-commoncrawl": "0.0.0",
|
|
69
68
|
"@ainyc/canonry-integration-google": "0.0.0",
|
|
70
|
-
"@ainyc/canonry-integration-
|
|
69
|
+
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
71
70
|
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
72
|
-
"@ainyc/canonry-
|
|
73
|
-
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
71
|
+
"@ainyc/canonry-integration-wordpress": "0.0.0",
|
|
74
72
|
"@ainyc/canonry-provider-local": "0.0.0",
|
|
75
73
|
"@ainyc/canonry-provider-perplexity": "0.0.0",
|
|
76
|
-
"@ainyc/canonry-provider-
|
|
74
|
+
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
75
|
+
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
76
|
+
"@ainyc/canonry-provider-claude": "0.0.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",
|