@momentumcms/plugins-analytics 0.4.0 → 0.4.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/index.cjs +289 -43
- package/index.js +285 -43
- package/lib/analytics-admin-routes.cjs +74 -9
- package/lib/analytics-admin-routes.js +72 -7
- package/lib/page-view-tracker.cjs +208 -0
- package/lib/page-view-tracker.js +189 -0
- package/package.json +9 -5
- package/src/index.d.ts +3 -1
- package/src/lib/analytics-config.types.d.ts +29 -0
- package/src/lib/analytics-plugin.d.ts +1 -1
- package/src/lib/collectors/page-view-collector.d.ts +29 -0
- package/src/lib/page-view-tracker.d.ts +21 -0
- package/src/lib/page-view-tracker.utils.d.ts +28 -0
- package/src/lib/utils/content-route-matcher.d.ts +42 -0
package/index.cjs
CHANGED
|
@@ -409,16 +409,20 @@ __export(src_exports, {
|
|
|
409
409
|
TrackingRules: () => TrackingRules,
|
|
410
410
|
analyticsPlugin: () => analyticsPlugin,
|
|
411
411
|
attachBlockTracking: () => attachBlockTracking,
|
|
412
|
+
compileContentRoutes: () => compileContentRoutes,
|
|
412
413
|
createAnalyticsMiddleware: () => createAnalyticsMiddleware,
|
|
413
414
|
createAnalyticsQueryRouter: () => createAnalyticsQueryRouter,
|
|
414
415
|
createApiCollectorMiddleware: () => createApiCollectorMiddleware,
|
|
415
416
|
createContentPerformanceRouter: () => createContentPerformanceRouter,
|
|
416
417
|
createIngestRouter: () => createIngestRouter,
|
|
418
|
+
createPageViewCollectorMiddleware: () => createPageViewCollectorMiddleware,
|
|
417
419
|
createRuleEngine: () => createRuleEngine,
|
|
418
420
|
createTracker: () => createTracker,
|
|
419
421
|
createTrackingRulesRouter: () => createTrackingRulesRouter,
|
|
420
422
|
injectBlockAnalyticsFields: () => injectBlockAnalyticsFields,
|
|
421
423
|
injectCollectionCollector: () => injectCollectionCollector,
|
|
424
|
+
isBot: () => isBot,
|
|
425
|
+
matchContentRoute: () => matchContentRoute,
|
|
422
426
|
parseUserAgent: () => parseUserAgent,
|
|
423
427
|
postgresAnalyticsAdapter: () => postgresAnalyticsAdapter
|
|
424
428
|
});
|
|
@@ -876,8 +880,210 @@ function createApiCollectorMiddleware(emitter) {
|
|
|
876
880
|
};
|
|
877
881
|
}
|
|
878
882
|
|
|
879
|
-
// libs/plugins/analytics/src/lib/
|
|
883
|
+
// libs/plugins/analytics/src/lib/collectors/page-view-collector.ts
|
|
880
884
|
var import_node_crypto3 = require("node:crypto");
|
|
885
|
+
|
|
886
|
+
// libs/plugins/analytics/src/lib/utils/content-route-matcher.ts
|
|
887
|
+
function escapeRegex(str) {
|
|
888
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
889
|
+
}
|
|
890
|
+
function compileContentRoute(collection, pattern) {
|
|
891
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
892
|
+
const paramNames = [];
|
|
893
|
+
let staticCount = 0;
|
|
894
|
+
const regexParts = segments.map((seg) => {
|
|
895
|
+
if (seg.startsWith(":")) {
|
|
896
|
+
paramNames.push(seg.slice(1));
|
|
897
|
+
return "([^/]+)";
|
|
898
|
+
}
|
|
899
|
+
staticCount++;
|
|
900
|
+
return escapeRegex(seg);
|
|
901
|
+
});
|
|
902
|
+
const regexStr = "^/" + regexParts.join("/") + "/?$";
|
|
903
|
+
return {
|
|
904
|
+
collection,
|
|
905
|
+
pattern,
|
|
906
|
+
regex: new RegExp(regexStr),
|
|
907
|
+
paramNames,
|
|
908
|
+
staticSegments: staticCount
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function compileContentRoutes(routes) {
|
|
912
|
+
const compiled = Object.entries(routes).map(
|
|
913
|
+
([collection, pattern]) => compileContentRoute(collection, pattern)
|
|
914
|
+
);
|
|
915
|
+
compiled.sort((a, b) => b.staticSegments - a.staticSegments);
|
|
916
|
+
return compiled;
|
|
917
|
+
}
|
|
918
|
+
function matchContentRoute(path, routes) {
|
|
919
|
+
for (const route of routes) {
|
|
920
|
+
const match = route.regex.exec(path);
|
|
921
|
+
if (match) {
|
|
922
|
+
const params = {};
|
|
923
|
+
for (let i = 0; i < route.paramNames.length; i++) {
|
|
924
|
+
params[route.paramNames[i]] = match[i + 1];
|
|
925
|
+
}
|
|
926
|
+
return { collection: route.collection, params };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// libs/plugins/analytics/src/lib/collectors/page-view-collector.ts
|
|
933
|
+
var DEFAULT_EXCLUDE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
934
|
+
".js",
|
|
935
|
+
".css",
|
|
936
|
+
".ico",
|
|
937
|
+
".png",
|
|
938
|
+
".jpg",
|
|
939
|
+
".jpeg",
|
|
940
|
+
".gif",
|
|
941
|
+
".svg",
|
|
942
|
+
".webp",
|
|
943
|
+
".avif",
|
|
944
|
+
".woff",
|
|
945
|
+
".woff2",
|
|
946
|
+
".ttf",
|
|
947
|
+
".eot",
|
|
948
|
+
".map",
|
|
949
|
+
".json",
|
|
950
|
+
".xml",
|
|
951
|
+
".txt",
|
|
952
|
+
".mp4",
|
|
953
|
+
".webm",
|
|
954
|
+
".ogg",
|
|
955
|
+
".mp3"
|
|
956
|
+
]);
|
|
957
|
+
var ALWAYS_EXCLUDED_PREFIXES = [
|
|
958
|
+
"/api/",
|
|
959
|
+
"/admin",
|
|
960
|
+
"/__vite",
|
|
961
|
+
"/@fs/",
|
|
962
|
+
"/@id/",
|
|
963
|
+
"/.analog/",
|
|
964
|
+
"/node_modules/"
|
|
965
|
+
];
|
|
966
|
+
var ALWAYS_EXCLUDED_EXACT = /* @__PURE__ */ new Set([
|
|
967
|
+
"/api",
|
|
968
|
+
"/favicon.ico",
|
|
969
|
+
"/robots.txt",
|
|
970
|
+
"/sitemap.xml",
|
|
971
|
+
"/sitemap-index.xml",
|
|
972
|
+
"/health",
|
|
973
|
+
"/healthz",
|
|
974
|
+
"/ready",
|
|
975
|
+
"/.well-known/security.txt"
|
|
976
|
+
]);
|
|
977
|
+
var BOT_PATTERN = /bot|crawl|spider|slurp|bingpreview|mediapartners|facebookexternalhit|linkedinbot|twitterbot|whatsapp|telegrambot|discordbot|applebot|duckduckbot|yandex|baidu|sogou|ia_archiver|semrush|ahref|mj12bot|dotbot|petalbot|bytespider/i;
|
|
978
|
+
function isBot(ua) {
|
|
979
|
+
if (!ua)
|
|
980
|
+
return false;
|
|
981
|
+
return BOT_PATTERN.test(ua);
|
|
982
|
+
}
|
|
983
|
+
function shouldExcludePath(path, excludeExtensions, excludePaths) {
|
|
984
|
+
if (ALWAYS_EXCLUDED_EXACT.has(path))
|
|
985
|
+
return true;
|
|
986
|
+
for (const prefix of ALWAYS_EXCLUDED_PREFIXES) {
|
|
987
|
+
if (path.startsWith(prefix))
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
for (const pattern of excludePaths) {
|
|
991
|
+
if (path.startsWith(pattern))
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
const dotIndex = path.lastIndexOf(".");
|
|
995
|
+
if (dotIndex !== -1) {
|
|
996
|
+
const ext = path.slice(dotIndex).toLowerCase();
|
|
997
|
+
if (excludeExtensions.has(ext))
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
function extractUserId(req) {
|
|
1003
|
+
if (!("user" in req))
|
|
1004
|
+
return void 0;
|
|
1005
|
+
const user = req["user"];
|
|
1006
|
+
if (user == null || typeof user !== "object")
|
|
1007
|
+
return void 0;
|
|
1008
|
+
if (!("id" in user))
|
|
1009
|
+
return void 0;
|
|
1010
|
+
const id = user.id;
|
|
1011
|
+
return typeof id === "string" ? id : void 0;
|
|
1012
|
+
}
|
|
1013
|
+
function createPageViewCollectorMiddleware(emitter, options = {}) {
|
|
1014
|
+
const excludeExtensions = options.excludeExtensions ? new Set(options.excludeExtensions) : DEFAULT_EXCLUDE_EXTENSIONS;
|
|
1015
|
+
const excludePaths = options.excludePaths ?? [];
|
|
1016
|
+
const onlySuccessful = options.onlySuccessful !== false;
|
|
1017
|
+
const trackBots = options.trackBots === true;
|
|
1018
|
+
const compiledRoutes = options.contentRoutes ? compileContentRoutes(options.contentRoutes) : void 0;
|
|
1019
|
+
return (req, res, next) => {
|
|
1020
|
+
if (req.method !== "GET") {
|
|
1021
|
+
next();
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const path = req.path;
|
|
1025
|
+
if (shouldExcludePath(path, excludeExtensions, excludePaths)) {
|
|
1026
|
+
next();
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const ua = req.headers["user-agent"];
|
|
1030
|
+
if (!trackBots && isBot(ua)) {
|
|
1031
|
+
next();
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const start = Date.now();
|
|
1035
|
+
res.once("finish", () => {
|
|
1036
|
+
if (onlySuccessful && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const duration = Date.now() - start;
|
|
1040
|
+
const parsed = parseUserAgent(ua);
|
|
1041
|
+
const refHeader = req.headers["referer"] ?? req.headers["referrer"];
|
|
1042
|
+
const referrer = Array.isArray(refHeader) ? refHeader[0] : refHeader;
|
|
1043
|
+
let contentCollection;
|
|
1044
|
+
let contentSlug;
|
|
1045
|
+
if (compiledRoutes) {
|
|
1046
|
+
const routeMatch = matchContentRoute(path, compiledRoutes);
|
|
1047
|
+
if (routeMatch) {
|
|
1048
|
+
contentCollection = routeMatch.collection;
|
|
1049
|
+
contentSlug = routeMatch.params["slug"];
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const event = {
|
|
1053
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
1054
|
+
category: "page",
|
|
1055
|
+
name: "page_view",
|
|
1056
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1057
|
+
userId: extractUserId(req),
|
|
1058
|
+
properties: {
|
|
1059
|
+
method: req.method,
|
|
1060
|
+
path,
|
|
1061
|
+
statusCode: res.statusCode,
|
|
1062
|
+
...contentCollection != null ? { collection: contentCollection } : {},
|
|
1063
|
+
...contentSlug != null ? { slug: contentSlug } : {}
|
|
1064
|
+
},
|
|
1065
|
+
context: {
|
|
1066
|
+
source: "server",
|
|
1067
|
+
url: req.originalUrl,
|
|
1068
|
+
referrer,
|
|
1069
|
+
userAgent: ua,
|
|
1070
|
+
ip: req.ip ?? req.socket.remoteAddress,
|
|
1071
|
+
device: parsed.device,
|
|
1072
|
+
browser: parsed.browser,
|
|
1073
|
+
os: parsed.os,
|
|
1074
|
+
duration,
|
|
1075
|
+
statusCode: res.statusCode,
|
|
1076
|
+
...contentCollection != null ? { collection: contentCollection } : {}
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
emitter(event);
|
|
1080
|
+
});
|
|
1081
|
+
next();
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// libs/plugins/analytics/src/lib/ingest-handler.ts
|
|
1086
|
+
var import_node_crypto4 = require("node:crypto");
|
|
881
1087
|
var import_express = require("express");
|
|
882
1088
|
|
|
883
1089
|
// libs/core/src/lib/collections/define-collection.ts
|
|
@@ -1158,7 +1364,7 @@ function createIngestRouter(options) {
|
|
|
1158
1364
|
}
|
|
1159
1365
|
const partial = raw;
|
|
1160
1366
|
const event = {
|
|
1161
|
-
id: (0,
|
|
1367
|
+
id: (0, import_node_crypto4.randomUUID)(),
|
|
1162
1368
|
category: partial.category ?? "custom",
|
|
1163
1369
|
name: partial.name ?? "unknown",
|
|
1164
1370
|
// Server-side timestamp (prevents client clock skew)
|
|
@@ -1764,6 +1970,42 @@ function matchesDocumentUrl(eventUrl, documentPath) {
|
|
|
1764
1970
|
}
|
|
1765
1971
|
return pathname === documentPath || pathname === `${documentPath}/`;
|
|
1766
1972
|
}
|
|
1973
|
+
async function queryEventsByDocument(queryFn, eventName, collection, documentId, documentPath, options) {
|
|
1974
|
+
const queryBase = {
|
|
1975
|
+
...options.category ? { category: options.category } : {},
|
|
1976
|
+
name: eventName,
|
|
1977
|
+
from: options.from,
|
|
1978
|
+
to: options.to,
|
|
1979
|
+
limit: 1e3
|
|
1980
|
+
};
|
|
1981
|
+
const [serverResult, allResult] = await Promise.all([
|
|
1982
|
+
queryFn({ ...queryBase, collection }),
|
|
1983
|
+
queryFn(queryBase)
|
|
1984
|
+
]);
|
|
1985
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1986
|
+
const results = [];
|
|
1987
|
+
for (const e of [...serverResult.events, ...allResult.events]) {
|
|
1988
|
+
if (seen.has(e.id))
|
|
1989
|
+
continue;
|
|
1990
|
+
seen.add(e.id);
|
|
1991
|
+
const matchesCollection = e.context.collection === collection || e.properties["collection"] === collection;
|
|
1992
|
+
const matchesSlug = typeof e.properties["slug"] === "string" && e.properties["slug"] === documentId;
|
|
1993
|
+
if (matchesCollection && matchesSlug)
|
|
1994
|
+
results.push(e);
|
|
1995
|
+
}
|
|
1996
|
+
if (results.length > 0)
|
|
1997
|
+
return results;
|
|
1998
|
+
const urlResult = await queryFn({ ...queryBase, search: documentPath });
|
|
1999
|
+
return urlResult.events.filter((e) => matchesDocumentUrl(e.context.url, documentPath));
|
|
2000
|
+
}
|
|
2001
|
+
function countByBlockType(events) {
|
|
2002
|
+
const map = /* @__PURE__ */ new Map();
|
|
2003
|
+
for (const event of events) {
|
|
2004
|
+
const bt = String(event.properties["blockType"] ?? "unknown");
|
|
2005
|
+
map.set(bt, (map.get(bt) ?? 0) + 1);
|
|
2006
|
+
}
|
|
2007
|
+
return map;
|
|
2008
|
+
}
|
|
1767
2009
|
function createContentPerformanceRouter(adapter) {
|
|
1768
2010
|
const router = (0, import_express3.Router)();
|
|
1769
2011
|
router.get("/content-performance", requireAdmin, async (req, res) => {
|
|
@@ -1782,16 +2024,15 @@ function createContentPerformanceRouter(adapter) {
|
|
|
1782
2024
|
return;
|
|
1783
2025
|
}
|
|
1784
2026
|
const documentPath = `/${collection}/${documentId}`;
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
(e) => matchesDocumentUrl(e.context.url, documentPath)
|
|
2027
|
+
const queryFn = adapter.query.bind(adapter);
|
|
2028
|
+
const dateRange = { from, to };
|
|
2029
|
+
const pageViewEvents = await queryEventsByDocument(
|
|
2030
|
+
queryFn,
|
|
2031
|
+
"page_view",
|
|
2032
|
+
collection,
|
|
2033
|
+
documentId,
|
|
2034
|
+
documentPath,
|
|
2035
|
+
{ category: "page", ...dateRange }
|
|
1795
2036
|
);
|
|
1796
2037
|
const visitorSet = /* @__PURE__ */ new Set();
|
|
1797
2038
|
const referrerMap = /* @__PURE__ */ new Map();
|
|
@@ -1806,38 +2047,26 @@ function createContentPerformanceRouter(adapter) {
|
|
|
1806
2047
|
}
|
|
1807
2048
|
let blockEngagement;
|
|
1808
2049
|
try {
|
|
1809
|
-
const [
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2050
|
+
const [impressionEvents, hoverEvents] = await Promise.all([
|
|
2051
|
+
queryEventsByDocument(
|
|
2052
|
+
queryFn,
|
|
2053
|
+
"block_impression",
|
|
2054
|
+
collection,
|
|
2055
|
+
documentId,
|
|
2056
|
+
documentPath,
|
|
2057
|
+
dateRange
|
|
2058
|
+
),
|
|
2059
|
+
queryEventsByDocument(
|
|
2060
|
+
queryFn,
|
|
2061
|
+
"block_hover",
|
|
2062
|
+
collection,
|
|
2063
|
+
documentId,
|
|
2064
|
+
documentPath,
|
|
2065
|
+
dateRange
|
|
2066
|
+
)
|
|
1824
2067
|
]);
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
);
|
|
1828
|
-
const hoverEvents = hoverResult.events.filter(
|
|
1829
|
-
(e) => matchesDocumentUrl(e.context.url, documentPath)
|
|
1830
|
-
);
|
|
1831
|
-
const impressionMap = /* @__PURE__ */ new Map();
|
|
1832
|
-
for (const event of impressionEvents) {
|
|
1833
|
-
const bt = String(event.properties["blockType"] ?? "unknown");
|
|
1834
|
-
impressionMap.set(bt, (impressionMap.get(bt) ?? 0) + 1);
|
|
1835
|
-
}
|
|
1836
|
-
const hoverMap = /* @__PURE__ */ new Map();
|
|
1837
|
-
for (const event of hoverEvents) {
|
|
1838
|
-
const bt = String(event.properties["blockType"] ?? "unknown");
|
|
1839
|
-
hoverMap.set(bt, (hoverMap.get(bt) ?? 0) + 1);
|
|
1840
|
-
}
|
|
2068
|
+
const impressionMap = countByBlockType(impressionEvents);
|
|
2069
|
+
const hoverMap = countByBlockType(hoverEvents);
|
|
1841
2070
|
const allTypes = /* @__PURE__ */ new Set([...impressionMap.keys(), ...hoverMap.keys()]);
|
|
1842
2071
|
if (allTypes.size > 0) {
|
|
1843
2072
|
blockEngagement = [];
|
|
@@ -2145,6 +2374,19 @@ function analyticsPlugin(config) {
|
|
|
2145
2374
|
position: "before-api"
|
|
2146
2375
|
});
|
|
2147
2376
|
}
|
|
2377
|
+
if (config.trackPageViews !== false) {
|
|
2378
|
+
const pageViewOptions = typeof config.trackPageViews === "object" ? config.trackPageViews : {};
|
|
2379
|
+
const pageViewCollector = createPageViewCollectorMiddleware(
|
|
2380
|
+
(event) => eventStore.add(event),
|
|
2381
|
+
pageViewOptions
|
|
2382
|
+
);
|
|
2383
|
+
registerMiddleware({
|
|
2384
|
+
path: "/",
|
|
2385
|
+
handler: pageViewCollector,
|
|
2386
|
+
position: "root"
|
|
2387
|
+
});
|
|
2388
|
+
logger.info("Page view tracking enabled");
|
|
2389
|
+
}
|
|
2148
2390
|
if (config.contentPerformance !== false) {
|
|
2149
2391
|
const contentPerfRouter = createContentPerformanceRouter(config.adapter);
|
|
2150
2392
|
registerMiddleware({
|
|
@@ -2230,16 +2472,20 @@ init_rule_engine();
|
|
|
2230
2472
|
TrackingRules,
|
|
2231
2473
|
analyticsPlugin,
|
|
2232
2474
|
attachBlockTracking,
|
|
2475
|
+
compileContentRoutes,
|
|
2233
2476
|
createAnalyticsMiddleware,
|
|
2234
2477
|
createAnalyticsQueryRouter,
|
|
2235
2478
|
createApiCollectorMiddleware,
|
|
2236
2479
|
createContentPerformanceRouter,
|
|
2237
2480
|
createIngestRouter,
|
|
2481
|
+
createPageViewCollectorMiddleware,
|
|
2238
2482
|
createRuleEngine,
|
|
2239
2483
|
createTracker,
|
|
2240
2484
|
createTrackingRulesRouter,
|
|
2241
2485
|
injectBlockAnalyticsFields,
|
|
2242
2486
|
injectCollectionCollector,
|
|
2487
|
+
isBot,
|
|
2488
|
+
matchContentRoute,
|
|
2243
2489
|
parseUserAgent,
|
|
2244
2490
|
postgresAnalyticsAdapter
|
|
2245
2491
|
});
|