@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.
@@ -296,6 +296,7 @@ import {
296
296
  PLATFORM_ID as PLATFORM_ID2
297
297
  } from "@angular/core";
298
298
  import { isPlatformBrowser as isPlatformBrowser2 } from "@angular/common";
299
+ import { Router, ActivatedRoute } from "@angular/router";
299
300
  import {
300
301
  Card as Card2,
301
302
  CardHeader as CardHeader2,
@@ -329,6 +330,8 @@ var init_analytics_dashboard_page = __esm({
329
330
  constructor() {
330
331
  this.analytics = inject2(AnalyticsService);
331
332
  this.platformId = inject2(PLATFORM_ID2);
333
+ this.router = inject2(Router);
334
+ this.route = inject2(ActivatedRoute);
332
335
  /** Date range options */
333
336
  this.dateRanges = [
334
337
  {
@@ -366,15 +369,12 @@ var init_analytics_dashboard_page = __esm({
366
369
  this.currentPage = signal3(1);
367
370
  /** Events per page */
368
371
  this.pageSize = 20;
369
- /** Filtered events based on selected category (client-side filter on top of server query) */
372
+ /** Events from server query (filtered server-side by category when selected) */
370
373
  this.filteredEvents = computed2(() => {
371
374
  const result = this.analytics.events();
372
375
  if (!result)
373
376
  return [];
374
- const cat = this.selectedCategory();
375
- if (cat === "all")
376
- return result.events;
377
- return result.events.filter((e) => e.category === cat);
377
+ return result.events;
378
378
  });
379
379
  /** Total pages for pagination */
380
380
  this.totalPages = computed2(() => {
@@ -394,6 +394,22 @@ var init_analytics_dashboard_page = __esm({
394
394
  ngOnInit() {
395
395
  if (!isPlatformBrowser2(this.platformId))
396
396
  return;
397
+ const params = this.route.snapshot.queryParams;
398
+ if (params["range"] && this.dateRanges.some((r) => r.value === params["range"])) {
399
+ this.selectedRange.set(params["range"]);
400
+ }
401
+ if (params["category"] && this.categoryFilters.some((c) => c.value === params["category"])) {
402
+ this.selectedCategory.set(params["category"]);
403
+ }
404
+ if (params["search"]) {
405
+ this.searchTerm.set(params["search"]);
406
+ }
407
+ if (params["page"]) {
408
+ const page = parseInt(params["page"], 10);
409
+ if (!isNaN(page) && page > 0) {
410
+ this.currentPage.set(page);
411
+ }
412
+ }
397
413
  void this.refresh();
398
414
  }
399
415
  /**
@@ -404,13 +420,15 @@ var init_analytics_dashboard_page = __esm({
404
420
  const from = dateRange?.getFrom();
405
421
  const search = this.searchTerm() || void 0;
406
422
  const page = this.currentPage();
423
+ const category = this.selectedCategory();
407
424
  await Promise.all([
408
425
  this.analytics.fetchSummary({ from }),
409
426
  this.analytics.queryEvents({
410
427
  limit: this.pageSize,
411
428
  page,
412
429
  from,
413
- search
430
+ search,
431
+ category: category !== "all" ? category : void 0
414
432
  })
415
433
  ]);
416
434
  }
@@ -420,13 +438,17 @@ var init_analytics_dashboard_page = __esm({
420
438
  setDateRange(range) {
421
439
  this.selectedRange.set(range.value);
422
440
  this.currentPage.set(1);
441
+ this.syncUrlParams();
423
442
  void this.refresh();
424
443
  }
425
444
  /**
426
- * Set category filter.
445
+ * Set category filter and re-query server.
427
446
  */
428
447
  setCategory(category) {
429
448
  this.selectedCategory.set(category);
449
+ this.currentPage.set(1);
450
+ this.syncUrlParams();
451
+ void this.refresh();
430
452
  }
431
453
  /**
432
454
  * Handle search input.
@@ -436,6 +458,7 @@ var init_analytics_dashboard_page = __esm({
436
458
  if (target instanceof HTMLInputElement) {
437
459
  this.searchTerm.set(target.value);
438
460
  this.currentPage.set(1);
461
+ this.syncUrlParams();
439
462
  void this.refresh();
440
463
  }
441
464
  }
@@ -444,8 +467,26 @@ var init_analytics_dashboard_page = __esm({
444
467
  */
445
468
  goToPage(page) {
446
469
  this.currentPage.set(page);
470
+ this.syncUrlParams();
447
471
  void this.refresh();
448
472
  }
473
+ /**
474
+ * Sync current filter state to URL query params.
475
+ */
476
+ syncUrlParams() {
477
+ const queryParams = {
478
+ range: this.selectedRange() !== "all" ? this.selectedRange() : null,
479
+ category: this.selectedCategory() !== "all" ? this.selectedCategory() : null,
480
+ search: this.searchTerm() || null,
481
+ page: this.currentPage() > 1 ? String(this.currentPage()) : null
482
+ };
483
+ void this.router.navigate([], {
484
+ relativeTo: this.route,
485
+ queryParams,
486
+ queryParamsHandling: "merge",
487
+ replaceUrl: true
488
+ });
489
+ }
449
490
  /**
450
491
  * Get total content operations count.
451
492
  */
@@ -1152,6 +1193,7 @@ import {
1152
1193
  PLATFORM_ID as PLATFORM_ID3
1153
1194
  } from "@angular/core";
1154
1195
  import { isPlatformBrowser as isPlatformBrowser3 } from "@angular/common";
1196
+ import { Router as Router2, ActivatedRoute as ActivatedRoute2 } from "@angular/router";
1155
1197
  import {
1156
1198
  Card as Card3,
1157
1199
  CardHeader as CardHeader3,
@@ -1180,6 +1222,8 @@ var init_content_performance_page = __esm({
1180
1222
  constructor() {
1181
1223
  this.service = inject3(ContentPerformanceService);
1182
1224
  this.platformId = inject3(PLATFORM_ID3);
1225
+ this.router = inject3(Router2);
1226
+ this.route = inject3(ActivatedRoute2);
1183
1227
  this.dateRanges = [
1184
1228
  {
1185
1229
  label: "24h",
@@ -1229,16 +1273,25 @@ var init_content_performance_page = __esm({
1229
1273
  ngOnInit() {
1230
1274
  if (!isPlatformBrowser3(this.platformId))
1231
1275
  return;
1276
+ const params = this.route.snapshot.queryParams;
1277
+ if (params["range"] && this.dateRanges.some((r) => r.value === params["range"])) {
1278
+ this.selectedRange.set(params["range"]);
1279
+ }
1280
+ if (params["search"]) {
1281
+ this.searchTerm.set(params["search"]);
1282
+ }
1232
1283
  void this.fetchData();
1233
1284
  }
1234
1285
  setDateRange(range) {
1235
1286
  this.selectedRange.set(range.value);
1236
1287
  this.expandedRow.set(null);
1288
+ this.syncUrlParams();
1237
1289
  void this.fetchData();
1238
1290
  }
1239
1291
  onSearch(event) {
1240
1292
  if (event.target instanceof HTMLInputElement) {
1241
1293
  this.searchTerm.set(event.target.value);
1294
+ this.syncUrlParams();
1242
1295
  }
1243
1296
  }
1244
1297
  toggleRow(url) {
@@ -1249,6 +1302,18 @@ var init_content_performance_page = __esm({
1249
1302
  const from = range?.getFrom();
1250
1303
  await this.service.fetchTopPages({ from });
1251
1304
  }
1305
+ syncUrlParams() {
1306
+ const queryParams = {
1307
+ range: this.selectedRange() !== "all" ? this.selectedRange() : null,
1308
+ search: this.searchTerm() || null
1309
+ };
1310
+ void this.router.navigate([], {
1311
+ relativeTo: this.route,
1312
+ queryParams,
1313
+ queryParamsHandling: "merge",
1314
+ replaceUrl: true
1315
+ });
1316
+ }
1252
1317
  };
1253
1318
  ContentPerformancePage = __decorateClass([
1254
1319
  Component3({
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var __decorateClass = (decorators, target, key, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
+ if (decorator = decorators[i])
23
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
24
+ if (kind && result)
25
+ __defProp(target, key, result);
26
+ return result;
27
+ };
28
+
29
+ // libs/plugins/analytics/src/lib/page-view-tracker.ts
30
+ var page_view_tracker_exports = {};
31
+ __export(page_view_tracker_exports, {
32
+ DEFAULT_EXCLUDE_PREFIXES: () => DEFAULT_EXCLUDE_PREFIXES,
33
+ PAGE_VIEW_TRACKING_CONFIG: () => PAGE_VIEW_TRACKING_CONFIG,
34
+ PageViewTrackerService: () => PageViewTrackerService,
35
+ buildPageViewEvent: () => buildPageViewEvent,
36
+ providePageViewTracking: () => providePageViewTracking,
37
+ shouldTrackNavigation: () => shouldTrackNavigation
38
+ });
39
+ module.exports = __toCommonJS(page_view_tracker_exports);
40
+ var import_core = require("@angular/core");
41
+ var import_common = require("@angular/common");
42
+ var import_router = require("@angular/router");
43
+ var import_http = require("@angular/common/http");
44
+ var import_rxjs = require("rxjs");
45
+
46
+ // libs/plugins/analytics/src/lib/utils/content-route-matcher.ts
47
+ function escapeRegex(str) {
48
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49
+ }
50
+ function compileContentRoute(collection, pattern) {
51
+ const segments = pattern.split("/").filter(Boolean);
52
+ const paramNames = [];
53
+ let staticCount = 0;
54
+ const regexParts = segments.map((seg) => {
55
+ if (seg.startsWith(":")) {
56
+ paramNames.push(seg.slice(1));
57
+ return "([^/]+)";
58
+ }
59
+ staticCount++;
60
+ return escapeRegex(seg);
61
+ });
62
+ const regexStr = "^/" + regexParts.join("/") + "/?$";
63
+ return {
64
+ collection,
65
+ pattern,
66
+ regex: new RegExp(regexStr),
67
+ paramNames,
68
+ staticSegments: staticCount
69
+ };
70
+ }
71
+ function compileContentRoutes(routes) {
72
+ const compiled = Object.entries(routes).map(
73
+ ([collection, pattern]) => compileContentRoute(collection, pattern)
74
+ );
75
+ compiled.sort((a, b) => b.staticSegments - a.staticSegments);
76
+ return compiled;
77
+ }
78
+ function matchContentRoute(path, routes) {
79
+ for (const route of routes) {
80
+ const match = route.regex.exec(path);
81
+ if (match) {
82
+ const params = {};
83
+ for (let i = 0; i < route.paramNames.length; i++) {
84
+ params[route.paramNames[i]] = match[i + 1];
85
+ }
86
+ return { collection: route.collection, params };
87
+ }
88
+ }
89
+ return void 0;
90
+ }
91
+
92
+ // libs/plugins/analytics/src/lib/page-view-tracker.utils.ts
93
+ var DEFAULT_EXCLUDE_PREFIXES = ["/admin", "/api/"];
94
+ function shouldTrackNavigation(url, isFirstNavigation, excludePrefixes) {
95
+ if (isFirstNavigation)
96
+ return false;
97
+ const path = url.split("?")[0].split("#")[0];
98
+ for (const prefix of excludePrefixes) {
99
+ if (path.startsWith(prefix))
100
+ return false;
101
+ }
102
+ return true;
103
+ }
104
+ function buildPageViewEvent(url, compiledRoutes) {
105
+ const path = url.split("?")[0].split("#")[0];
106
+ const properties = { path };
107
+ if (compiledRoutes) {
108
+ const routeMatch = matchContentRoute(path, compiledRoutes);
109
+ if (routeMatch) {
110
+ properties["collection"] = routeMatch.collection;
111
+ properties["slug"] = routeMatch.params["slug"];
112
+ }
113
+ }
114
+ return {
115
+ name: "page_view",
116
+ category: "page",
117
+ properties
118
+ };
119
+ }
120
+
121
+ // libs/plugins/analytics/src/lib/page-view-tracker.ts
122
+ var PAGE_VIEW_TRACKING_CONFIG = new import_core.InjectionToken(
123
+ "PAGE_VIEW_TRACKING_CONFIG"
124
+ );
125
+ function getOrCreateStorageId(storage, key) {
126
+ let id = storage.getItem(key);
127
+ if (!id) {
128
+ id = Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
129
+ storage.setItem(key, id);
130
+ }
131
+ return id;
132
+ }
133
+ var PageViewTrackerService = class {
134
+ constructor() {
135
+ this.router = (0, import_core.inject)(import_router.Router);
136
+ this.http = (0, import_core.inject)(import_http.HttpClient);
137
+ this.platformId = (0, import_core.inject)(import_core.PLATFORM_ID);
138
+ this.destroyRef = (0, import_core.inject)(import_core.DestroyRef);
139
+ this.config = (0, import_core.inject)(PAGE_VIEW_TRACKING_CONFIG);
140
+ this.doc = (0, import_core.inject)(import_common.DOCUMENT);
141
+ this.isFirstNavigation = true;
142
+ this.endpoint = this.config.endpoint ?? "/api/analytics/collect";
143
+ this.compiledRoutes = this.config.contentRoutes ? compileContentRoutes(this.config.contentRoutes) : void 0;
144
+ this.excludePrefixes = this.config.excludePrefixes ? [...DEFAULT_EXCLUDE_PREFIXES, ...this.config.excludePrefixes] : DEFAULT_EXCLUDE_PREFIXES;
145
+ if (!(0, import_common.isPlatformBrowser)(this.platformId))
146
+ return;
147
+ const sub = this.router.events.pipe((0, import_rxjs.filter)((e) => e instanceof import_router.NavigationEnd)).subscribe((event) => {
148
+ const url = event.urlAfterRedirects;
149
+ if (!shouldTrackNavigation(url, this.isFirstNavigation, this.excludePrefixes)) {
150
+ this.isFirstNavigation = false;
151
+ return;
152
+ }
153
+ this.isFirstNavigation = false;
154
+ this.trackPageView(url);
155
+ });
156
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
157
+ }
158
+ trackPageView(url) {
159
+ const eventPayload = buildPageViewEvent(url, this.compiledRoutes);
160
+ const win = this.doc.defaultView;
161
+ let visitorId;
162
+ let sessionId;
163
+ try {
164
+ if (win?.localStorage)
165
+ visitorId = getOrCreateStorageId(win.localStorage, "_m_vid");
166
+ if (win?.sessionStorage)
167
+ sessionId = getOrCreateStorageId(win.sessionStorage, "_m_sid");
168
+ } catch {
169
+ }
170
+ const clientEvent = {
171
+ ...eventPayload,
172
+ context: {
173
+ url: win?.location?.href ?? "",
174
+ referrer: this.doc.referrer
175
+ },
176
+ visitorId,
177
+ sessionId
178
+ };
179
+ this.http.post(this.endpoint, { events: [clientEvent] }).subscribe();
180
+ }
181
+ };
182
+ PageViewTrackerService = __decorateClass([
183
+ (0, import_core.Injectable)()
184
+ ], PageViewTrackerService);
185
+ function providePageViewTracking(config) {
186
+ return (0, import_core.makeEnvironmentProviders)([
187
+ { provide: PAGE_VIEW_TRACKING_CONFIG, useValue: config },
188
+ PageViewTrackerService,
189
+ {
190
+ provide: import_core.ENVIRONMENT_INITIALIZER,
191
+ multi: true,
192
+ useFactory: () => {
193
+ (0, import_core.inject)(PageViewTrackerService);
194
+ return () => {
195
+ };
196
+ }
197
+ }
198
+ ]);
199
+ }
200
+ // Annotate the CommonJS export names for ESM import in node:
201
+ 0 && (module.exports = {
202
+ DEFAULT_EXCLUDE_PREFIXES,
203
+ PAGE_VIEW_TRACKING_CONFIG,
204
+ PageViewTrackerService,
205
+ buildPageViewEvent,
206
+ providePageViewTracking,
207
+ shouldTrackNavigation
208
+ });
@@ -0,0 +1,189 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result)
9
+ __defProp(target, key, result);
10
+ return result;
11
+ };
12
+
13
+ // libs/plugins/analytics/src/lib/page-view-tracker.ts
14
+ import {
15
+ Injectable,
16
+ InjectionToken,
17
+ inject,
18
+ DestroyRef,
19
+ PLATFORM_ID,
20
+ makeEnvironmentProviders,
21
+ ENVIRONMENT_INITIALIZER
22
+ } from "@angular/core";
23
+ import { DOCUMENT, isPlatformBrowser } from "@angular/common";
24
+ import { Router, NavigationEnd } from "@angular/router";
25
+ import { HttpClient } from "@angular/common/http";
26
+ import { filter } from "rxjs";
27
+
28
+ // libs/plugins/analytics/src/lib/utils/content-route-matcher.ts
29
+ function escapeRegex(str) {
30
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ }
32
+ function compileContentRoute(collection, pattern) {
33
+ const segments = pattern.split("/").filter(Boolean);
34
+ const paramNames = [];
35
+ let staticCount = 0;
36
+ const regexParts = segments.map((seg) => {
37
+ if (seg.startsWith(":")) {
38
+ paramNames.push(seg.slice(1));
39
+ return "([^/]+)";
40
+ }
41
+ staticCount++;
42
+ return escapeRegex(seg);
43
+ });
44
+ const regexStr = "^/" + regexParts.join("/") + "/?$";
45
+ return {
46
+ collection,
47
+ pattern,
48
+ regex: new RegExp(regexStr),
49
+ paramNames,
50
+ staticSegments: staticCount
51
+ };
52
+ }
53
+ function compileContentRoutes(routes) {
54
+ const compiled = Object.entries(routes).map(
55
+ ([collection, pattern]) => compileContentRoute(collection, pattern)
56
+ );
57
+ compiled.sort((a, b) => b.staticSegments - a.staticSegments);
58
+ return compiled;
59
+ }
60
+ function matchContentRoute(path, routes) {
61
+ for (const route of routes) {
62
+ const match = route.regex.exec(path);
63
+ if (match) {
64
+ const params = {};
65
+ for (let i = 0; i < route.paramNames.length; i++) {
66
+ params[route.paramNames[i]] = match[i + 1];
67
+ }
68
+ return { collection: route.collection, params };
69
+ }
70
+ }
71
+ return void 0;
72
+ }
73
+
74
+ // libs/plugins/analytics/src/lib/page-view-tracker.utils.ts
75
+ var DEFAULT_EXCLUDE_PREFIXES = ["/admin", "/api/"];
76
+ function shouldTrackNavigation(url, isFirstNavigation, excludePrefixes) {
77
+ if (isFirstNavigation)
78
+ return false;
79
+ const path = url.split("?")[0].split("#")[0];
80
+ for (const prefix of excludePrefixes) {
81
+ if (path.startsWith(prefix))
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ function buildPageViewEvent(url, compiledRoutes) {
87
+ const path = url.split("?")[0].split("#")[0];
88
+ const properties = { path };
89
+ if (compiledRoutes) {
90
+ const routeMatch = matchContentRoute(path, compiledRoutes);
91
+ if (routeMatch) {
92
+ properties["collection"] = routeMatch.collection;
93
+ properties["slug"] = routeMatch.params["slug"];
94
+ }
95
+ }
96
+ return {
97
+ name: "page_view",
98
+ category: "page",
99
+ properties
100
+ };
101
+ }
102
+
103
+ // libs/plugins/analytics/src/lib/page-view-tracker.ts
104
+ var PAGE_VIEW_TRACKING_CONFIG = new InjectionToken(
105
+ "PAGE_VIEW_TRACKING_CONFIG"
106
+ );
107
+ function getOrCreateStorageId(storage, key) {
108
+ let id = storage.getItem(key);
109
+ if (!id) {
110
+ id = Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
111
+ storage.setItem(key, id);
112
+ }
113
+ return id;
114
+ }
115
+ var PageViewTrackerService = class {
116
+ constructor() {
117
+ this.router = inject(Router);
118
+ this.http = inject(HttpClient);
119
+ this.platformId = inject(PLATFORM_ID);
120
+ this.destroyRef = inject(DestroyRef);
121
+ this.config = inject(PAGE_VIEW_TRACKING_CONFIG);
122
+ this.doc = inject(DOCUMENT);
123
+ this.isFirstNavigation = true;
124
+ this.endpoint = this.config.endpoint ?? "/api/analytics/collect";
125
+ this.compiledRoutes = this.config.contentRoutes ? compileContentRoutes(this.config.contentRoutes) : void 0;
126
+ this.excludePrefixes = this.config.excludePrefixes ? [...DEFAULT_EXCLUDE_PREFIXES, ...this.config.excludePrefixes] : DEFAULT_EXCLUDE_PREFIXES;
127
+ if (!isPlatformBrowser(this.platformId))
128
+ return;
129
+ const sub = this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe((event) => {
130
+ const url = event.urlAfterRedirects;
131
+ if (!shouldTrackNavigation(url, this.isFirstNavigation, this.excludePrefixes)) {
132
+ this.isFirstNavigation = false;
133
+ return;
134
+ }
135
+ this.isFirstNavigation = false;
136
+ this.trackPageView(url);
137
+ });
138
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
139
+ }
140
+ trackPageView(url) {
141
+ const eventPayload = buildPageViewEvent(url, this.compiledRoutes);
142
+ const win = this.doc.defaultView;
143
+ let visitorId;
144
+ let sessionId;
145
+ try {
146
+ if (win?.localStorage)
147
+ visitorId = getOrCreateStorageId(win.localStorage, "_m_vid");
148
+ if (win?.sessionStorage)
149
+ sessionId = getOrCreateStorageId(win.sessionStorage, "_m_sid");
150
+ } catch {
151
+ }
152
+ const clientEvent = {
153
+ ...eventPayload,
154
+ context: {
155
+ url: win?.location?.href ?? "",
156
+ referrer: this.doc.referrer
157
+ },
158
+ visitorId,
159
+ sessionId
160
+ };
161
+ this.http.post(this.endpoint, { events: [clientEvent] }).subscribe();
162
+ }
163
+ };
164
+ PageViewTrackerService = __decorateClass([
165
+ Injectable()
166
+ ], PageViewTrackerService);
167
+ function providePageViewTracking(config) {
168
+ return makeEnvironmentProviders([
169
+ { provide: PAGE_VIEW_TRACKING_CONFIG, useValue: config },
170
+ PageViewTrackerService,
171
+ {
172
+ provide: ENVIRONMENT_INITIALIZER,
173
+ multi: true,
174
+ useFactory: () => {
175
+ inject(PageViewTrackerService);
176
+ return () => {
177
+ };
178
+ }
179
+ }
180
+ ]);
181
+ }
182
+ export {
183
+ DEFAULT_EXCLUDE_PREFIXES,
184
+ PAGE_VIEW_TRACKING_CONFIG,
185
+ PageViewTrackerService,
186
+ buildPageViewEvent,
187
+ providePageViewTracking,
188
+ shouldTrackNavigation
189
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/plugins-analytics",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Analytics and content tracking plugin for Momentum CMS",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -41,13 +41,17 @@
41
41
  "./client": {
42
42
  "types": "./src/lib/client/tracker.d.ts",
43
43
  "default": "./lib/client/tracker.cjs"
44
+ },
45
+ "./page-tracker": {
46
+ "types": "./src/lib/page-view-tracker.d.ts",
47
+ "default": "./lib/page-view-tracker.cjs"
44
48
  }
45
49
  },
46
50
  "dependencies": {
47
- "@momentumcms/core": "0.4.0",
48
- "@momentumcms/logger": "0.4.0",
49
- "@momentumcms/plugins-core": "0.4.0",
50
- "@momentumcms/server-core": "0.4.0",
51
+ "@momentumcms/core": "0.4.1",
52
+ "@momentumcms/logger": "0.4.1",
53
+ "@momentumcms/plugins-core": "0.4.1",
54
+ "@momentumcms/server-core": "0.4.1",
51
55
  "express": "^4.21.0"
52
56
  },
53
57
  "peerDependencies": {
package/src/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export type { AnalyticsEvent, AnalyticsCategory, AnalyticsContext, AnalyticsQueryOptions, AnalyticsQueryResult, } from './lib/analytics-event.types';
2
- export type { AnalyticsAdapter, AnalyticsConfig } from './lib/analytics-config.types';
2
+ export type { AnalyticsAdapter, AnalyticsConfig, PageViewTrackingOptions, } from './lib/analytics-config.types';
3
3
  export { EventStore, type EventStoreOptions } from './lib/event-store';
4
4
  export { injectCollectionCollector, type AnalyticsEmitter, type CollectionCollectorOptions, } from './lib/collectors/collection-collector';
5
5
  export { createApiCollectorMiddleware } from './lib/collectors/api-collector';
6
+ export { createPageViewCollectorMiddleware, isBot, type PageViewEmitter, } from './lib/collectors/page-view-collector';
7
+ export { compileContentRoutes, matchContentRoute, type CompiledContentRoute, type ContentRouteMatch, } from './lib/utils/content-route-matcher';
6
8
  export { createIngestRouter, type IngestHandlerOptions } from './lib/ingest-handler';
7
9
  export { MemoryAnalyticsAdapter } from './lib/adapters/memory-adapter';
8
10
  export { postgresAnalyticsAdapter, type PostgresAnalyticsAdapterOptions, } from './lib/adapters/postgres-adapter';
@@ -16,6 +16,27 @@ export interface AnalyticsAdapter {
16
16
  /** Flush pending events and close connections */
17
17
  shutdown?(): Promise<void>;
18
18
  }
19
+ /**
20
+ * Options for server-side page view tracking.
21
+ */
22
+ export interface PageViewTrackingOptions {
23
+ /** Additional path prefixes to exclude from tracking. */
24
+ excludePaths?: string[];
25
+ /** File extensions to ignore (overrides defaults when provided). */
26
+ excludeExtensions?: string[];
27
+ /** Only track responses with 2xx status codes. @default true */
28
+ onlySuccessful?: boolean;
29
+ /** Whether to track bot traffic (Googlebot, Bingbot, etc.). @default false */
30
+ trackBots?: boolean;
31
+ /**
32
+ * Map collection slugs to URL patterns for content attribution.
33
+ * When a page view URL matches a pattern, the event is enriched with
34
+ * collection and slug metadata, enabling per-document analytics.
35
+ * Patterns use Express-style `:param` syntax.
36
+ * @example { articles: '/articles/:slug', pages: '/:slug' }
37
+ */
38
+ contentRoutes?: Record<string, string>;
39
+ }
19
40
  /**
20
41
  * Analytics configuration.
21
42
  */
@@ -30,6 +51,14 @@ export interface AnalyticsConfig {
30
51
  trackApi?: boolean;
31
52
  /** Admin action tracking. @default true */
32
53
  trackAdmin?: boolean;
54
+ /**
55
+ * Server-side page view tracking (SSR page renders).
56
+ * - `true` (default): enable with default settings
57
+ * - `false`: disable
58
+ * - object: override page view tracking options
59
+ * @default true
60
+ */
61
+ trackPageViews?: boolean | PageViewTrackingOptions;
33
62
  /** Client-side ingest endpoint path. @default '/api/analytics/collect' */
34
63
  ingestPath?: string;
35
64
  /** Rate limit for ingest endpoint (requests per minute per IP). @default 100 */
@@ -2,7 +2,7 @@
2
2
  * Analytics Plugin
3
3
  *
4
4
  * A Momentum CMS plugin that wires all analytics collectors together.
5
- * Tracks collection CRUD, API requests, and accepts client-side events.
5
+ * Tracks collection CRUD, API requests, page views, and accepts client-side events.
6
6
  *
7
7
  * @example
8
8
  * ```typescript