@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.js CHANGED
@@ -831,8 +831,210 @@ function createApiCollectorMiddleware(emitter) {
831
831
  };
832
832
  }
833
833
 
834
- // libs/plugins/analytics/src/lib/ingest-handler.ts
834
+ // libs/plugins/analytics/src/lib/collectors/page-view-collector.ts
835
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";
836
1038
  import { Router as createRouter } from "express";
837
1039
 
838
1040
  // libs/core/src/lib/collections/define-collection.ts
@@ -1126,7 +1328,7 @@ function createIngestRouter(options) {
1126
1328
  }
1127
1329
  const partial = raw;
1128
1330
  const event = {
1129
- id: randomUUID3(),
1331
+ id: randomUUID4(),
1130
1332
  category: partial.category ?? "custom",
1131
1333
  name: partial.name ?? "unknown",
1132
1334
  // Server-side timestamp (prevents client clock skew)
@@ -1732,6 +1934,42 @@ function matchesDocumentUrl(eventUrl, documentPath) {
1732
1934
  }
1733
1935
  return pathname === documentPath || pathname === `${documentPath}/`;
1734
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
+ }
1735
1973
  function createContentPerformanceRouter(adapter) {
1736
1974
  const router = Router2();
1737
1975
  router.get("/content-performance", requireAdmin, async (req, res) => {
@@ -1750,16 +1988,15 @@ function createContentPerformanceRouter(adapter) {
1750
1988
  return;
1751
1989
  }
1752
1990
  const documentPath = `/${collection}/${documentId}`;
1753
- const pageViewResult = await adapter.query({
1754
- category: "page",
1755
- name: "page_view",
1756
- search: documentPath,
1757
- from,
1758
- to,
1759
- limit: 1e3
1760
- });
1761
- const pageViewEvents = pageViewResult.events.filter(
1762
- (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 }
1763
2000
  );
1764
2001
  const visitorSet = /* @__PURE__ */ new Set();
1765
2002
  const referrerMap = /* @__PURE__ */ new Map();
@@ -1774,38 +2011,26 @@ function createContentPerformanceRouter(adapter) {
1774
2011
  }
1775
2012
  let blockEngagement;
1776
2013
  try {
1777
- const [impressionResult, hoverResult] = await Promise.all([
1778
- adapter.query({
1779
- name: "block_impression",
1780
- search: documentPath,
1781
- from,
1782
- to,
1783
- limit: 1e3
1784
- }),
1785
- adapter.query({
1786
- name: "block_hover",
1787
- search: documentPath,
1788
- from,
1789
- to,
1790
- limit: 1e3
1791
- })
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
+ )
1792
2031
  ]);
1793
- const impressionEvents = impressionResult.events.filter(
1794
- (e) => matchesDocumentUrl(e.context.url, documentPath)
1795
- );
1796
- const hoverEvents = hoverResult.events.filter(
1797
- (e) => matchesDocumentUrl(e.context.url, documentPath)
1798
- );
1799
- const impressionMap = /* @__PURE__ */ new Map();
1800
- for (const event of impressionEvents) {
1801
- const bt = String(event.properties["blockType"] ?? "unknown");
1802
- impressionMap.set(bt, (impressionMap.get(bt) ?? 0) + 1);
1803
- }
1804
- const hoverMap = /* @__PURE__ */ new Map();
1805
- for (const event of hoverEvents) {
1806
- const bt = String(event.properties["blockType"] ?? "unknown");
1807
- hoverMap.set(bt, (hoverMap.get(bt) ?? 0) + 1);
1808
- }
2032
+ const impressionMap = countByBlockType(impressionEvents);
2033
+ const hoverMap = countByBlockType(hoverEvents);
1809
2034
  const allTypes = /* @__PURE__ */ new Set([...impressionMap.keys(), ...hoverMap.keys()]);
1810
2035
  if (allTypes.size > 0) {
1811
2036
  blockEngagement = [];
@@ -2113,6 +2338,19 @@ function analyticsPlugin(config) {
2113
2338
  position: "before-api"
2114
2339
  });
2115
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
+ }
2116
2354
  if (config.contentPerformance !== false) {
2117
2355
  const contentPerfRouter = createContentPerformanceRouter(config.adapter);
2118
2356
  registerMiddleware({
@@ -2197,16 +2435,20 @@ export {
2197
2435
  TrackingRules,
2198
2436
  analyticsPlugin,
2199
2437
  attachBlockTracking,
2438
+ compileContentRoutes,
2200
2439
  createAnalyticsMiddleware,
2201
2440
  createAnalyticsQueryRouter,
2202
2441
  createApiCollectorMiddleware,
2203
2442
  createContentPerformanceRouter,
2204
2443
  createIngestRouter,
2444
+ createPageViewCollectorMiddleware,
2205
2445
  createRuleEngine,
2206
2446
  createTracker,
2207
2447
  createTrackingRulesRouter,
2208
2448
  injectBlockAnalyticsFields,
2209
2449
  injectCollectionCollector,
2450
+ isBot,
2451
+ matchContentRoute,
2210
2452
  parseUserAgent,
2211
2453
  postgresAnalyticsAdapter
2212
2454
  };
@@ -291,12 +291,13 @@ var analytics_dashboard_page_exports = {};
291
291
  __export(analytics_dashboard_page_exports, {
292
292
  AnalyticsDashboardPage: () => AnalyticsDashboardPage
293
293
  });
294
- var import_core4, import_common2, import_ui2, import_core5, import_outline2, AnalyticsDashboardPage;
294
+ var import_core4, import_common2, import_router, import_ui2, import_core5, import_outline2, AnalyticsDashboardPage;
295
295
  var init_analytics_dashboard_page = __esm({
296
296
  "libs/plugins/analytics/src/lib/dashboard/analytics-dashboard.page.ts"() {
297
297
  "use strict";
298
298
  import_core4 = require("@angular/core");
299
299
  import_common2 = require("@angular/common");
300
+ import_router = require("@angular/router");
300
301
  import_ui2 = require("@momentumcms/ui");
301
302
  import_core5 = require("@ng-icons/core");
302
303
  import_outline2 = require("@ng-icons/heroicons/outline");
@@ -306,6 +307,8 @@ var init_analytics_dashboard_page = __esm({
306
307
  constructor() {
307
308
  this.analytics = (0, import_core4.inject)(AnalyticsService);
308
309
  this.platformId = (0, import_core4.inject)(import_core4.PLATFORM_ID);
310
+ this.router = (0, import_core4.inject)(import_router.Router);
311
+ this.route = (0, import_core4.inject)(import_router.ActivatedRoute);
309
312
  /** Date range options */
310
313
  this.dateRanges = [
311
314
  {
@@ -343,15 +346,12 @@ var init_analytics_dashboard_page = __esm({
343
346
  this.currentPage = (0, import_core4.signal)(1);
344
347
  /** Events per page */
345
348
  this.pageSize = 20;
346
- /** Filtered events based on selected category (client-side filter on top of server query) */
349
+ /** Events from server query (filtered server-side by category when selected) */
347
350
  this.filteredEvents = (0, import_core4.computed)(() => {
348
351
  const result = this.analytics.events();
349
352
  if (!result)
350
353
  return [];
351
- const cat = this.selectedCategory();
352
- if (cat === "all")
353
- return result.events;
354
- return result.events.filter((e) => e.category === cat);
354
+ return result.events;
355
355
  });
356
356
  /** Total pages for pagination */
357
357
  this.totalPages = (0, import_core4.computed)(() => {
@@ -371,6 +371,22 @@ var init_analytics_dashboard_page = __esm({
371
371
  ngOnInit() {
372
372
  if (!(0, import_common2.isPlatformBrowser)(this.platformId))
373
373
  return;
374
+ const params = this.route.snapshot.queryParams;
375
+ if (params["range"] && this.dateRanges.some((r) => r.value === params["range"])) {
376
+ this.selectedRange.set(params["range"]);
377
+ }
378
+ if (params["category"] && this.categoryFilters.some((c) => c.value === params["category"])) {
379
+ this.selectedCategory.set(params["category"]);
380
+ }
381
+ if (params["search"]) {
382
+ this.searchTerm.set(params["search"]);
383
+ }
384
+ if (params["page"]) {
385
+ const page = parseInt(params["page"], 10);
386
+ if (!isNaN(page) && page > 0) {
387
+ this.currentPage.set(page);
388
+ }
389
+ }
374
390
  void this.refresh();
375
391
  }
376
392
  /**
@@ -381,13 +397,15 @@ var init_analytics_dashboard_page = __esm({
381
397
  const from = dateRange?.getFrom();
382
398
  const search = this.searchTerm() || void 0;
383
399
  const page = this.currentPage();
400
+ const category = this.selectedCategory();
384
401
  await Promise.all([
385
402
  this.analytics.fetchSummary({ from }),
386
403
  this.analytics.queryEvents({
387
404
  limit: this.pageSize,
388
405
  page,
389
406
  from,
390
- search
407
+ search,
408
+ category: category !== "all" ? category : void 0
391
409
  })
392
410
  ]);
393
411
  }
@@ -397,13 +415,17 @@ var init_analytics_dashboard_page = __esm({
397
415
  setDateRange(range) {
398
416
  this.selectedRange.set(range.value);
399
417
  this.currentPage.set(1);
418
+ this.syncUrlParams();
400
419
  void this.refresh();
401
420
  }
402
421
  /**
403
- * Set category filter.
422
+ * Set category filter and re-query server.
404
423
  */
405
424
  setCategory(category) {
406
425
  this.selectedCategory.set(category);
426
+ this.currentPage.set(1);
427
+ this.syncUrlParams();
428
+ void this.refresh();
407
429
  }
408
430
  /**
409
431
  * Handle search input.
@@ -413,6 +435,7 @@ var init_analytics_dashboard_page = __esm({
413
435
  if (target instanceof HTMLInputElement) {
414
436
  this.searchTerm.set(target.value);
415
437
  this.currentPage.set(1);
438
+ this.syncUrlParams();
416
439
  void this.refresh();
417
440
  }
418
441
  }
@@ -421,8 +444,26 @@ var init_analytics_dashboard_page = __esm({
421
444
  */
422
445
  goToPage(page) {
423
446
  this.currentPage.set(page);
447
+ this.syncUrlParams();
424
448
  void this.refresh();
425
449
  }
450
+ /**
451
+ * Sync current filter state to URL query params.
452
+ */
453
+ syncUrlParams() {
454
+ const queryParams = {
455
+ range: this.selectedRange() !== "all" ? this.selectedRange() : null,
456
+ category: this.selectedCategory() !== "all" ? this.selectedCategory() : null,
457
+ search: this.searchTerm() || null,
458
+ page: this.currentPage() > 1 ? String(this.currentPage()) : null
459
+ };
460
+ void this.router.navigate([], {
461
+ relativeTo: this.route,
462
+ queryParams,
463
+ queryParamsHandling: "merge",
464
+ replaceUrl: true
465
+ });
466
+ }
426
467
  /**
427
468
  * Get total content operations count.
428
469
  */
@@ -1120,12 +1161,13 @@ var content_performance_page_exports = {};
1120
1161
  __export(content_performance_page_exports, {
1121
1162
  ContentPerformancePage: () => ContentPerformancePage
1122
1163
  });
1123
- var import_core7, import_common3, import_ui3, import_core8, import_outline3, ContentPerformancePage;
1164
+ var import_core7, import_common3, import_router2, import_ui3, import_core8, import_outline3, ContentPerformancePage;
1124
1165
  var init_content_performance_page = __esm({
1125
1166
  "libs/plugins/analytics/src/lib/dashboard/content-performance.page.ts"() {
1126
1167
  "use strict";
1127
1168
  import_core7 = require("@angular/core");
1128
1169
  import_common3 = require("@angular/common");
1170
+ import_router2 = require("@angular/router");
1129
1171
  import_ui3 = require("@momentumcms/ui");
1130
1172
  import_core8 = require("@ng-icons/core");
1131
1173
  import_outline3 = require("@ng-icons/heroicons/outline");
@@ -1134,6 +1176,8 @@ var init_content_performance_page = __esm({
1134
1176
  constructor() {
1135
1177
  this.service = (0, import_core7.inject)(ContentPerformanceService);
1136
1178
  this.platformId = (0, import_core7.inject)(import_core7.PLATFORM_ID);
1179
+ this.router = (0, import_core7.inject)(import_router2.Router);
1180
+ this.route = (0, import_core7.inject)(import_router2.ActivatedRoute);
1137
1181
  this.dateRanges = [
1138
1182
  {
1139
1183
  label: "24h",
@@ -1183,16 +1227,25 @@ var init_content_performance_page = __esm({
1183
1227
  ngOnInit() {
1184
1228
  if (!(0, import_common3.isPlatformBrowser)(this.platformId))
1185
1229
  return;
1230
+ const params = this.route.snapshot.queryParams;
1231
+ if (params["range"] && this.dateRanges.some((r) => r.value === params["range"])) {
1232
+ this.selectedRange.set(params["range"]);
1233
+ }
1234
+ if (params["search"]) {
1235
+ this.searchTerm.set(params["search"]);
1236
+ }
1186
1237
  void this.fetchData();
1187
1238
  }
1188
1239
  setDateRange(range) {
1189
1240
  this.selectedRange.set(range.value);
1190
1241
  this.expandedRow.set(null);
1242
+ this.syncUrlParams();
1191
1243
  void this.fetchData();
1192
1244
  }
1193
1245
  onSearch(event) {
1194
1246
  if (event.target instanceof HTMLInputElement) {
1195
1247
  this.searchTerm.set(event.target.value);
1248
+ this.syncUrlParams();
1196
1249
  }
1197
1250
  }
1198
1251
  toggleRow(url) {
@@ -1203,6 +1256,18 @@ var init_content_performance_page = __esm({
1203
1256
  const from = range?.getFrom();
1204
1257
  await this.service.fetchTopPages({ from });
1205
1258
  }
1259
+ syncUrlParams() {
1260
+ const queryParams = {
1261
+ range: this.selectedRange() !== "all" ? this.selectedRange() : null,
1262
+ search: this.searchTerm() || null
1263
+ };
1264
+ void this.router.navigate([], {
1265
+ relativeTo: this.route,
1266
+ queryParams,
1267
+ queryParamsHandling: "merge",
1268
+ replaceUrl: true
1269
+ });
1270
+ }
1206
1271
  };
1207
1272
  ContentPerformancePage = __decorateClass([
1208
1273
  (0, import_core7.Component)({