@momentumcms/plugins-analytics 0.3.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 +295 -45
- package/index.js +291 -45
- package/lib/analytics-admin-routes.cjs +2108 -0
- package/lib/analytics-admin-routes.js +2195 -0
- package/lib/client/tracker.cjs +541 -0
- package/lib/client/tracker.js +523 -0
- package/lib/collectors/block-field-injector.cjs +206 -0
- package/lib/collectors/block-field-injector.js +179 -0
- package/lib/page-view-tracker.cjs +208 -0
- package/lib/page-view-tracker.js +189 -0
- package/package.json +64 -55
- package/src/index.d.ts +3 -1
- package/src/lib/analytics-admin-routes.d.ts +3 -0
- 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/CHANGELOG.md +0 -99
- package/LICENSE +0 -21
package/index.js
CHANGED
|
@@ -14,6 +14,10 @@ __export(block_tracker_exports, {
|
|
|
14
14
|
attachBlockTracking: () => attachBlockTracking
|
|
15
15
|
});
|
|
16
16
|
function attachBlockTracking(tracker, container) {
|
|
17
|
+
if (typeof document === "undefined" || !document.body) {
|
|
18
|
+
return () => {
|
|
19
|
+
};
|
|
20
|
+
}
|
|
17
21
|
const root = container ?? document.body;
|
|
18
22
|
const impressionsSeen = /* @__PURE__ */ new Set();
|
|
19
23
|
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
@@ -827,8 +831,210 @@ function createApiCollectorMiddleware(emitter) {
|
|
|
827
831
|
};
|
|
828
832
|
}
|
|
829
833
|
|
|
830
|
-
// libs/plugins/analytics/src/lib/
|
|
834
|
+
// libs/plugins/analytics/src/lib/collectors/page-view-collector.ts
|
|
831
835
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
836
|
+
|
|
837
|
+
// libs/plugins/analytics/src/lib/utils/content-route-matcher.ts
|
|
838
|
+
function escapeRegex(str) {
|
|
839
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
840
|
+
}
|
|
841
|
+
function compileContentRoute(collection, pattern) {
|
|
842
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
843
|
+
const paramNames = [];
|
|
844
|
+
let staticCount = 0;
|
|
845
|
+
const regexParts = segments.map((seg) => {
|
|
846
|
+
if (seg.startsWith(":")) {
|
|
847
|
+
paramNames.push(seg.slice(1));
|
|
848
|
+
return "([^/]+)";
|
|
849
|
+
}
|
|
850
|
+
staticCount++;
|
|
851
|
+
return escapeRegex(seg);
|
|
852
|
+
});
|
|
853
|
+
const regexStr = "^/" + regexParts.join("/") + "/?$";
|
|
854
|
+
return {
|
|
855
|
+
collection,
|
|
856
|
+
pattern,
|
|
857
|
+
regex: new RegExp(regexStr),
|
|
858
|
+
paramNames,
|
|
859
|
+
staticSegments: staticCount
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function compileContentRoutes(routes) {
|
|
863
|
+
const compiled = Object.entries(routes).map(
|
|
864
|
+
([collection, pattern]) => compileContentRoute(collection, pattern)
|
|
865
|
+
);
|
|
866
|
+
compiled.sort((a, b) => b.staticSegments - a.staticSegments);
|
|
867
|
+
return compiled;
|
|
868
|
+
}
|
|
869
|
+
function matchContentRoute(path, routes) {
|
|
870
|
+
for (const route of routes) {
|
|
871
|
+
const match = route.regex.exec(path);
|
|
872
|
+
if (match) {
|
|
873
|
+
const params = {};
|
|
874
|
+
for (let i = 0; i < route.paramNames.length; i++) {
|
|
875
|
+
params[route.paramNames[i]] = match[i + 1];
|
|
876
|
+
}
|
|
877
|
+
return { collection: route.collection, params };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return void 0;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// libs/plugins/analytics/src/lib/collectors/page-view-collector.ts
|
|
884
|
+
var DEFAULT_EXCLUDE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
885
|
+
".js",
|
|
886
|
+
".css",
|
|
887
|
+
".ico",
|
|
888
|
+
".png",
|
|
889
|
+
".jpg",
|
|
890
|
+
".jpeg",
|
|
891
|
+
".gif",
|
|
892
|
+
".svg",
|
|
893
|
+
".webp",
|
|
894
|
+
".avif",
|
|
895
|
+
".woff",
|
|
896
|
+
".woff2",
|
|
897
|
+
".ttf",
|
|
898
|
+
".eot",
|
|
899
|
+
".map",
|
|
900
|
+
".json",
|
|
901
|
+
".xml",
|
|
902
|
+
".txt",
|
|
903
|
+
".mp4",
|
|
904
|
+
".webm",
|
|
905
|
+
".ogg",
|
|
906
|
+
".mp3"
|
|
907
|
+
]);
|
|
908
|
+
var ALWAYS_EXCLUDED_PREFIXES = [
|
|
909
|
+
"/api/",
|
|
910
|
+
"/admin",
|
|
911
|
+
"/__vite",
|
|
912
|
+
"/@fs/",
|
|
913
|
+
"/@id/",
|
|
914
|
+
"/.analog/",
|
|
915
|
+
"/node_modules/"
|
|
916
|
+
];
|
|
917
|
+
var ALWAYS_EXCLUDED_EXACT = /* @__PURE__ */ new Set([
|
|
918
|
+
"/api",
|
|
919
|
+
"/favicon.ico",
|
|
920
|
+
"/robots.txt",
|
|
921
|
+
"/sitemap.xml",
|
|
922
|
+
"/sitemap-index.xml",
|
|
923
|
+
"/health",
|
|
924
|
+
"/healthz",
|
|
925
|
+
"/ready",
|
|
926
|
+
"/.well-known/security.txt"
|
|
927
|
+
]);
|
|
928
|
+
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;
|
|
929
|
+
function isBot(ua) {
|
|
930
|
+
if (!ua)
|
|
931
|
+
return false;
|
|
932
|
+
return BOT_PATTERN.test(ua);
|
|
933
|
+
}
|
|
934
|
+
function shouldExcludePath(path, excludeExtensions, excludePaths) {
|
|
935
|
+
if (ALWAYS_EXCLUDED_EXACT.has(path))
|
|
936
|
+
return true;
|
|
937
|
+
for (const prefix of ALWAYS_EXCLUDED_PREFIXES) {
|
|
938
|
+
if (path.startsWith(prefix))
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
for (const pattern of excludePaths) {
|
|
942
|
+
if (path.startsWith(pattern))
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
const dotIndex = path.lastIndexOf(".");
|
|
946
|
+
if (dotIndex !== -1) {
|
|
947
|
+
const ext = path.slice(dotIndex).toLowerCase();
|
|
948
|
+
if (excludeExtensions.has(ext))
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
function extractUserId(req) {
|
|
954
|
+
if (!("user" in req))
|
|
955
|
+
return void 0;
|
|
956
|
+
const user = req["user"];
|
|
957
|
+
if (user == null || typeof user !== "object")
|
|
958
|
+
return void 0;
|
|
959
|
+
if (!("id" in user))
|
|
960
|
+
return void 0;
|
|
961
|
+
const id = user.id;
|
|
962
|
+
return typeof id === "string" ? id : void 0;
|
|
963
|
+
}
|
|
964
|
+
function createPageViewCollectorMiddleware(emitter, options = {}) {
|
|
965
|
+
const excludeExtensions = options.excludeExtensions ? new Set(options.excludeExtensions) : DEFAULT_EXCLUDE_EXTENSIONS;
|
|
966
|
+
const excludePaths = options.excludePaths ?? [];
|
|
967
|
+
const onlySuccessful = options.onlySuccessful !== false;
|
|
968
|
+
const trackBots = options.trackBots === true;
|
|
969
|
+
const compiledRoutes = options.contentRoutes ? compileContentRoutes(options.contentRoutes) : void 0;
|
|
970
|
+
return (req, res, next) => {
|
|
971
|
+
if (req.method !== "GET") {
|
|
972
|
+
next();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const path = req.path;
|
|
976
|
+
if (shouldExcludePath(path, excludeExtensions, excludePaths)) {
|
|
977
|
+
next();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const ua = req.headers["user-agent"];
|
|
981
|
+
if (!trackBots && isBot(ua)) {
|
|
982
|
+
next();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const start = Date.now();
|
|
986
|
+
res.once("finish", () => {
|
|
987
|
+
if (onlySuccessful && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const duration = Date.now() - start;
|
|
991
|
+
const parsed = parseUserAgent(ua);
|
|
992
|
+
const refHeader = req.headers["referer"] ?? req.headers["referrer"];
|
|
993
|
+
const referrer = Array.isArray(refHeader) ? refHeader[0] : refHeader;
|
|
994
|
+
let contentCollection;
|
|
995
|
+
let contentSlug;
|
|
996
|
+
if (compiledRoutes) {
|
|
997
|
+
const routeMatch = matchContentRoute(path, compiledRoutes);
|
|
998
|
+
if (routeMatch) {
|
|
999
|
+
contentCollection = routeMatch.collection;
|
|
1000
|
+
contentSlug = routeMatch.params["slug"];
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const event = {
|
|
1004
|
+
id: randomUUID3(),
|
|
1005
|
+
category: "page",
|
|
1006
|
+
name: "page_view",
|
|
1007
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1008
|
+
userId: extractUserId(req),
|
|
1009
|
+
properties: {
|
|
1010
|
+
method: req.method,
|
|
1011
|
+
path,
|
|
1012
|
+
statusCode: res.statusCode,
|
|
1013
|
+
...contentCollection != null ? { collection: contentCollection } : {},
|
|
1014
|
+
...contentSlug != null ? { slug: contentSlug } : {}
|
|
1015
|
+
},
|
|
1016
|
+
context: {
|
|
1017
|
+
source: "server",
|
|
1018
|
+
url: req.originalUrl,
|
|
1019
|
+
referrer,
|
|
1020
|
+
userAgent: ua,
|
|
1021
|
+
ip: req.ip ?? req.socket.remoteAddress,
|
|
1022
|
+
device: parsed.device,
|
|
1023
|
+
browser: parsed.browser,
|
|
1024
|
+
os: parsed.os,
|
|
1025
|
+
duration,
|
|
1026
|
+
statusCode: res.statusCode,
|
|
1027
|
+
...contentCollection != null ? { collection: contentCollection } : {}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
emitter(event);
|
|
1031
|
+
});
|
|
1032
|
+
next();
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// libs/plugins/analytics/src/lib/ingest-handler.ts
|
|
1037
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
832
1038
|
import { Router as createRouter } from "express";
|
|
833
1039
|
|
|
834
1040
|
// libs/core/src/lib/collections/define-collection.ts
|
|
@@ -1122,7 +1328,7 @@ function createIngestRouter(options) {
|
|
|
1122
1328
|
}
|
|
1123
1329
|
const partial = raw;
|
|
1124
1330
|
const event = {
|
|
1125
|
-
id:
|
|
1331
|
+
id: randomUUID4(),
|
|
1126
1332
|
category: partial.category ?? "custom",
|
|
1127
1333
|
name: partial.name ?? "unknown",
|
|
1128
1334
|
// Server-side timestamp (prevents client clock skew)
|
|
@@ -1728,6 +1934,42 @@ function matchesDocumentUrl(eventUrl, documentPath) {
|
|
|
1728
1934
|
}
|
|
1729
1935
|
return pathname === documentPath || pathname === `${documentPath}/`;
|
|
1730
1936
|
}
|
|
1937
|
+
async function queryEventsByDocument(queryFn, eventName, collection, documentId, documentPath, options) {
|
|
1938
|
+
const queryBase = {
|
|
1939
|
+
...options.category ? { category: options.category } : {},
|
|
1940
|
+
name: eventName,
|
|
1941
|
+
from: options.from,
|
|
1942
|
+
to: options.to,
|
|
1943
|
+
limit: 1e3
|
|
1944
|
+
};
|
|
1945
|
+
const [serverResult, allResult] = await Promise.all([
|
|
1946
|
+
queryFn({ ...queryBase, collection }),
|
|
1947
|
+
queryFn(queryBase)
|
|
1948
|
+
]);
|
|
1949
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1950
|
+
const results = [];
|
|
1951
|
+
for (const e of [...serverResult.events, ...allResult.events]) {
|
|
1952
|
+
if (seen.has(e.id))
|
|
1953
|
+
continue;
|
|
1954
|
+
seen.add(e.id);
|
|
1955
|
+
const matchesCollection = e.context.collection === collection || e.properties["collection"] === collection;
|
|
1956
|
+
const matchesSlug = typeof e.properties["slug"] === "string" && e.properties["slug"] === documentId;
|
|
1957
|
+
if (matchesCollection && matchesSlug)
|
|
1958
|
+
results.push(e);
|
|
1959
|
+
}
|
|
1960
|
+
if (results.length > 0)
|
|
1961
|
+
return results;
|
|
1962
|
+
const urlResult = await queryFn({ ...queryBase, search: documentPath });
|
|
1963
|
+
return urlResult.events.filter((e) => matchesDocumentUrl(e.context.url, documentPath));
|
|
1964
|
+
}
|
|
1965
|
+
function countByBlockType(events) {
|
|
1966
|
+
const map = /* @__PURE__ */ new Map();
|
|
1967
|
+
for (const event of events) {
|
|
1968
|
+
const bt = String(event.properties["blockType"] ?? "unknown");
|
|
1969
|
+
map.set(bt, (map.get(bt) ?? 0) + 1);
|
|
1970
|
+
}
|
|
1971
|
+
return map;
|
|
1972
|
+
}
|
|
1731
1973
|
function createContentPerformanceRouter(adapter) {
|
|
1732
1974
|
const router = Router2();
|
|
1733
1975
|
router.get("/content-performance", requireAdmin, async (req, res) => {
|
|
@@ -1746,16 +1988,15 @@ function createContentPerformanceRouter(adapter) {
|
|
|
1746
1988
|
return;
|
|
1747
1989
|
}
|
|
1748
1990
|
const documentPath = `/${collection}/${documentId}`;
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
(e) => matchesDocumentUrl(e.context.url, documentPath)
|
|
1991
|
+
const queryFn = adapter.query.bind(adapter);
|
|
1992
|
+
const dateRange = { from, to };
|
|
1993
|
+
const pageViewEvents = await queryEventsByDocument(
|
|
1994
|
+
queryFn,
|
|
1995
|
+
"page_view",
|
|
1996
|
+
collection,
|
|
1997
|
+
documentId,
|
|
1998
|
+
documentPath,
|
|
1999
|
+
{ category: "page", ...dateRange }
|
|
1759
2000
|
);
|
|
1760
2001
|
const visitorSet = /* @__PURE__ */ new Set();
|
|
1761
2002
|
const referrerMap = /* @__PURE__ */ new Map();
|
|
@@ -1770,38 +2011,26 @@ function createContentPerformanceRouter(adapter) {
|
|
|
1770
2011
|
}
|
|
1771
2012
|
let blockEngagement;
|
|
1772
2013
|
try {
|
|
1773
|
-
const [
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2014
|
+
const [impressionEvents, hoverEvents] = await Promise.all([
|
|
2015
|
+
queryEventsByDocument(
|
|
2016
|
+
queryFn,
|
|
2017
|
+
"block_impression",
|
|
2018
|
+
collection,
|
|
2019
|
+
documentId,
|
|
2020
|
+
documentPath,
|
|
2021
|
+
dateRange
|
|
2022
|
+
),
|
|
2023
|
+
queryEventsByDocument(
|
|
2024
|
+
queryFn,
|
|
2025
|
+
"block_hover",
|
|
2026
|
+
collection,
|
|
2027
|
+
documentId,
|
|
2028
|
+
documentPath,
|
|
2029
|
+
dateRange
|
|
2030
|
+
)
|
|
1788
2031
|
]);
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
);
|
|
1792
|
-
const hoverEvents = hoverResult.events.filter(
|
|
1793
|
-
(e) => matchesDocumentUrl(e.context.url, documentPath)
|
|
1794
|
-
);
|
|
1795
|
-
const impressionMap = /* @__PURE__ */ new Map();
|
|
1796
|
-
for (const event of impressionEvents) {
|
|
1797
|
-
const bt = String(event.properties["blockType"] ?? "unknown");
|
|
1798
|
-
impressionMap.set(bt, (impressionMap.get(bt) ?? 0) + 1);
|
|
1799
|
-
}
|
|
1800
|
-
const hoverMap = /* @__PURE__ */ new Map();
|
|
1801
|
-
for (const event of hoverEvents) {
|
|
1802
|
-
const bt = String(event.properties["blockType"] ?? "unknown");
|
|
1803
|
-
hoverMap.set(bt, (hoverMap.get(bt) ?? 0) + 1);
|
|
1804
|
-
}
|
|
2032
|
+
const impressionMap = countByBlockType(impressionEvents);
|
|
2033
|
+
const hoverMap = countByBlockType(hoverEvents);
|
|
1805
2034
|
const allTypes = /* @__PURE__ */ new Set([...impressionMap.keys(), ...hoverMap.keys()]);
|
|
1806
2035
|
if (allTypes.size > 0) {
|
|
1807
2036
|
blockEngagement = [];
|
|
@@ -2054,11 +2283,11 @@ function analyticsPlugin(config) {
|
|
|
2054
2283
|
// Browser-safe import paths for the admin config generator
|
|
2055
2284
|
browserImports: {
|
|
2056
2285
|
adminRoutes: {
|
|
2057
|
-
path: "@momentumcms/plugins
|
|
2286
|
+
path: "@momentumcms/plugins-analytics/admin-routes",
|
|
2058
2287
|
exportName: "analyticsAdminRoutes"
|
|
2059
2288
|
},
|
|
2060
2289
|
modifyCollections: {
|
|
2061
|
-
path: "@momentumcms/plugins
|
|
2290
|
+
path: "@momentumcms/plugins-analytics/block-fields",
|
|
2062
2291
|
exportName: "injectBlockAnalyticsFields"
|
|
2063
2292
|
}
|
|
2064
2293
|
},
|
|
@@ -2109,6 +2338,19 @@ function analyticsPlugin(config) {
|
|
|
2109
2338
|
position: "before-api"
|
|
2110
2339
|
});
|
|
2111
2340
|
}
|
|
2341
|
+
if (config.trackPageViews !== false) {
|
|
2342
|
+
const pageViewOptions = typeof config.trackPageViews === "object" ? config.trackPageViews : {};
|
|
2343
|
+
const pageViewCollector = createPageViewCollectorMiddleware(
|
|
2344
|
+
(event) => eventStore.add(event),
|
|
2345
|
+
pageViewOptions
|
|
2346
|
+
);
|
|
2347
|
+
registerMiddleware({
|
|
2348
|
+
path: "/",
|
|
2349
|
+
handler: pageViewCollector,
|
|
2350
|
+
position: "root"
|
|
2351
|
+
});
|
|
2352
|
+
logger.info("Page view tracking enabled");
|
|
2353
|
+
}
|
|
2112
2354
|
if (config.contentPerformance !== false) {
|
|
2113
2355
|
const contentPerfRouter = createContentPerformanceRouter(config.adapter);
|
|
2114
2356
|
registerMiddleware({
|
|
@@ -2193,16 +2435,20 @@ export {
|
|
|
2193
2435
|
TrackingRules,
|
|
2194
2436
|
analyticsPlugin,
|
|
2195
2437
|
attachBlockTracking,
|
|
2438
|
+
compileContentRoutes,
|
|
2196
2439
|
createAnalyticsMiddleware,
|
|
2197
2440
|
createAnalyticsQueryRouter,
|
|
2198
2441
|
createApiCollectorMiddleware,
|
|
2199
2442
|
createContentPerformanceRouter,
|
|
2200
2443
|
createIngestRouter,
|
|
2444
|
+
createPageViewCollectorMiddleware,
|
|
2201
2445
|
createRuleEngine,
|
|
2202
2446
|
createTracker,
|
|
2203
2447
|
createTrackingRulesRouter,
|
|
2204
2448
|
injectBlockAnalyticsFields,
|
|
2205
2449
|
injectCollectionCollector,
|
|
2450
|
+
isBot,
|
|
2451
|
+
matchContentRoute,
|
|
2206
2452
|
parseUserAgent,
|
|
2207
2453
|
postgresAnalyticsAdapter
|
|
2208
2454
|
};
|