@jx3box/jx3box-ui 2.4.4 → 2.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-ui",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "description": "JX3BOX Vue3 UI",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -10,14 +10,114 @@ import {
10
10
  installVueRouterAnalytics,
11
11
  } from "@jx3box/jx3box-common/js/analytics.js";
12
12
 
13
- const DEFAULT_CONFIG_ENDPOINT = "/api/cms/system/traffic/config";
14
- const DEFAULT_BATCH_ENDPOINT = "/api/cms/system/traffic/visits/batch";
13
+ const DEFAULT_CONFIG_ENDPOINT = "https://cms.jx3box.com/api/cms/system/traffic/config";
14
+ const DEFAULT_BATCH_ENDPOINT = "https://cms.jx3box.com/api/cms/system/traffic/visits/batch";
15
15
  const DEFAULT_QUEUE_STORAGE_KEY = "jx3box:analytics:traffic:queue:v1";
16
16
  const DEFAULT_BLOCK_STORAGE_KEY = "jx3box:analytics:traffic:block:v1";
17
17
  const EMBEDDED_SURFACES = new Set(["app", "miniprogram", "pc_game", "mobile_game"]);
18
18
  const COMMON_HEADER_INSTALLATIONS = new WeakMap();
19
19
  const COMMON_HEADER_TRAFFIC_PROJECT = "jx3box-ui";
20
20
 
21
+ function normalizeRoutePath(value) {
22
+ let path = String(value || "")
23
+ .split(/[?#]/, 1)[0]
24
+ .trim();
25
+ if (!path) return "";
26
+ if (!path.startsWith("/")) path = `/${path}`;
27
+ path = path.replace(/\/{2,}/g, "/");
28
+ return path.length > 1 ? path.replace(/\/+$/, "") : path;
29
+ }
30
+
31
+ function normalizeVueRouteTemplate(value) {
32
+ const path = normalizeRoutePath(value);
33
+ if (!path || path === "/") return path;
34
+ return `/${path
35
+ .slice(1)
36
+ .split("/")
37
+ .map(function (segment) {
38
+ if (!segment.startsWith(":")) return segment;
39
+ const name = segment.slice(1).match(/^[A-Za-z_][A-Za-z0-9_]*/)?.[0];
40
+ if (!name) return segment;
41
+ // Backend rules own parameter validation. Vue Router custom regexes
42
+ // such as :id(\d+) are local matching details and must normalize to
43
+ // the registered cross-client template :id.
44
+ return `:${name}${segment.endsWith("?") ? "?" : ""}`;
45
+ })
46
+ .join("/")}`;
47
+ }
48
+
49
+ function routeTemplate(route) {
50
+ const source = route || {};
51
+ const meta = source.meta && typeof source.meta === "object" ? source.meta : {};
52
+ const analytics = meta.analytics && typeof meta.analytics === "object" ? meta.analytics : {};
53
+ const matched = Array.isArray(source.matched) ? source.matched : [];
54
+ const record = matched.length ? matched[matched.length - 1] : null;
55
+ return normalizeVueRouteTemplate(
56
+ analytics.route_pattern || analytics.routePattern || (record && record.path) || source.path || ""
57
+ );
58
+ }
59
+
60
+ function routerBase(router) {
61
+ return normalizeRoutePath(router?.options?.history?.base || router?.history?.base || "/") || "/";
62
+ }
63
+
64
+ function resolveAbsoluteRoutePattern(router, route, runtime) {
65
+ const pattern = routeTemplate(route);
66
+ if (!pattern) return "";
67
+ const base = routerBase(router);
68
+ if (base === "/" || pattern === base || pattern.startsWith(`${base}/`)) return pattern;
69
+
70
+ // Multi-entry PC projects mount their Router below paths such as /macro or /pet.
71
+ // Vue Router exposes only / and /:id inside that base, so retain the route
72
+ // template while restoring its public prefix. location.pathname is used only
73
+ // to verify that this Router currently owns the browser path; real IDs never
74
+ // replace the template or enter the long-term page key.
75
+ const pathname = normalizeRoutePath(runtime?.location?.pathname || "");
76
+ if (pathname && pathname !== base && !pathname.startsWith(`${base}/`)) return pattern;
77
+ return pattern === "/" ? base : `${base}${pattern}`;
78
+ }
79
+
80
+ function createAbsoluteRouteRouter(router, runtime) {
81
+ if (!router || typeof router.afterEach !== "function") return router;
82
+ const adaptRoute = function (route) {
83
+ if (!route) return route;
84
+ const routePattern = resolveAbsoluteRoutePattern(router, route, runtime);
85
+ if (!routePattern) return route;
86
+ const meta = route.meta && typeof route.meta === "object" ? route.meta : {};
87
+ const analytics = meta.analytics && typeof meta.analytics === "object" ? meta.analytics : {};
88
+ return Object.assign({}, route, {
89
+ meta: Object.assign({}, meta, {
90
+ analytics: Object.assign({}, analytics, { route_pattern: routePattern }),
91
+ }),
92
+ });
93
+ };
94
+ const adapter = Object.create(router);
95
+ adapter.afterEach = function (handler) {
96
+ return router.afterEach(function (to, from, failure) {
97
+ return handler(adaptRoute(to), adaptRoute(from), failure);
98
+ });
99
+ };
100
+ if (typeof router.isReady === "function") adapter.isReady = router.isReady.bind(router);
101
+ const currentRoute = router.currentRoute;
102
+ if (currentRoute && typeof currentRoute === "object" && "value" in currentRoute) {
103
+ adapter.currentRoute = {};
104
+ Object.defineProperty(adapter.currentRoute, "value", {
105
+ enumerable: true,
106
+ get: function () {
107
+ return adaptRoute(currentRoute.value);
108
+ },
109
+ });
110
+ } else {
111
+ Object.defineProperty(adapter, "currentRoute", {
112
+ enumerable: true,
113
+ get: function () {
114
+ return adaptRoute(router.currentRoute);
115
+ },
116
+ });
117
+ }
118
+ return adapter;
119
+ }
120
+
21
121
  function queryValue(runtime, key) {
22
122
  const location = (runtime && runtime.location) || {};
23
123
  const sources = [location.search, String(location.hash || "").split("?")[1]];
@@ -465,12 +565,13 @@ function installCommonHeaderTrafficAnalytics(options) {
465
565
  const surface = settings.surface || "pc_web";
466
566
  if (surface !== "pc_web" && surface !== "mobile_web") return Promise.resolve(null);
467
567
  const project = COMMON_HEADER_TRAFFIC_PROJECT;
568
+ const analyticsRouter = createAbsoluteRouteRouter(router, runtime);
468
569
 
469
570
  const task = Promise.resolve(typeof router.isReady === "function" ? router.isReady() : undefined)
470
571
  .then(function () {
471
572
  const collector = createJx3boxTrafficAnalytics({
472
573
  runtime,
473
- router,
574
+ router: analyticsRouter,
474
575
  project,
475
576
  product: "jx3box",
476
577
  client: surface,
@@ -497,8 +598,10 @@ export {
497
598
  DEFAULT_CONFIG_ENDPOINT,
498
599
  DEFAULT_QUEUE_STORAGE_KEY,
499
600
  createJx3boxTrafficAnalytics,
601
+ createAbsoluteRouteRouter,
500
602
  installCommonHeaderTrafficAnalytics,
501
603
  normalizeRecipientDomain,
502
604
  normalizeTrafficPermission,
503
605
  resolvePcTrafficGameClient,
606
+ resolveAbsoluteRoutePattern,
504
607
  };
@@ -24,10 +24,12 @@ const { createEventQueue, createQueueStorage, createTrafficSink } = require("@jx
24
24
  const {
25
25
  DEFAULT_BLOCK_STORAGE_KEY,
26
26
  DEFAULT_QUEUE_STORAGE_KEY,
27
+ createAbsoluteRouteRouter,
27
28
  createJx3boxTrafficAnalytics,
28
29
  normalizeRecipientDomain,
29
30
  normalizeTrafficPermission,
30
31
  resolvePcTrafficGameClient,
32
+ resolveAbsoluteRoutePattern,
31
33
  } = require("../src/utils/traffic-analytics.js");
32
34
  const trafficSource = fs.readFileSync(path.resolve(__dirname, "../src/utils/traffic-analytics.js"), "utf8");
33
35
 
@@ -91,6 +93,39 @@ function createRouter() {
91
93
  };
92
94
  }
93
95
 
96
+ test("CommonHeader restores a multi-entry Router base without exposing real path IDs", () => {
97
+ const router = createRouter();
98
+ router.options = { history: { base: "/macro/" } };
99
+ const runtime = { location: { pathname: "/macro/123" } };
100
+ const detail = {
101
+ name: "single",
102
+ matched: [{ path: "/:id(\\d+)" }],
103
+ meta: {},
104
+ params: { id: "123" },
105
+ };
106
+ assert.equal(resolveAbsoluteRoutePattern(router, detail, runtime), "/macro/:id");
107
+ assert.equal(resolveAbsoluteRoutePattern(router, { matched: [{ path: "/" }], meta: {} }, runtime), "/macro");
108
+
109
+ const adapted = createAbsoluteRouteRouter(router, runtime);
110
+ let received = null;
111
+ adapted.afterEach((to) => {
112
+ received = to;
113
+ });
114
+ router.navigate(detail);
115
+ assert.equal(received.meta.analytics.route_pattern, "/macro/:id");
116
+ assert.equal(received.meta.analytics.route_pattern.includes("123"), false);
117
+ });
118
+
119
+ test("CommonHeader does not duplicate an already absolute route template", () => {
120
+ const router = createRouter();
121
+ router.options = { history: { base: "/pet" } };
122
+ const runtime = { location: { pathname: "/pet/456" } };
123
+ assert.equal(
124
+ resolveAbsoluteRoutePattern(router, { matched: [{ path: "/pet/:id" }], meta: {} }, runtime),
125
+ "/pet/:id"
126
+ );
127
+ });
128
+
94
129
  function createRuntime(options) {
95
130
  const settings = options || {};
96
131
  const listeners = new Map();
@@ -236,7 +271,7 @@ test("headless opt-in emits the official Traffic fixture once and honors per-id
236
271
  ids: [fixture.canonical_event.page_view_id, fixture.canonical_event.event_id],
237
272
  async fetch(url, options) {
238
273
  requests.push({ url, options: options || {} });
239
- if (String(url).startsWith("/api/cms/system/traffic/config")) {
274
+ if (String(url).startsWith("https://cms.jx3box.com/api/cms/system/traffic/config")) {
240
275
  return createResponse(200, {
241
276
  code: 0,
242
277
  data: {
@@ -313,7 +348,7 @@ test("headless opt-in emits the official Traffic fixture once and honors per-id
313
348
  await waitForRouter();
314
349
 
315
350
  assert.equal(requests.length, 1, "navigation should request config but defer Traffic until finalization");
316
- assert.match(requests[0].url, /^\/api\/cms\/system\/traffic\/config\?/);
351
+ assert.match(requests[0].url, /^https:\/\/cms\.jx3box\.com\/api\/cms\/system\/traffic\/config\?/);
317
352
  assert.equal(requests[0].url.includes("must-stay-local"), false);
318
353
  assert.equal(requests[0].url.includes("section=home"), false);
319
354
  assert.equal(traffic.getState().analytics.queue.pending, 1);
@@ -387,7 +422,7 @@ test("embedded hosts use the backend recipient domain in both Config and envelop
387
422
  const runtime = createRuntime({
388
423
  async fetch(url, options) {
389
424
  requests.push({ url, options: options || {} });
390
- if (String(url).startsWith("/api/cms/system/traffic/config")) {
425
+ if (String(url).startsWith("https://cms.jx3box.com/api/cms/system/traffic/config")) {
391
426
  return createResponse(200, {
392
427
  code: 0,
393
428
  data: {
@@ -450,7 +485,7 @@ test("an explicit unknown permission pauses in memory until the host unblocks",
450
485
  localStorage,
451
486
  async fetch(url, options) {
452
487
  requests.push({ url, options: options || {} });
453
- if (String(url).startsWith("/api/cms/system/traffic/config")) {
488
+ if (String(url).startsWith("https://cms.jx3box.com/api/cms/system/traffic/config")) {
454
489
  return createResponse(200, {
455
490
  code: 0,
456
491
  data: {
@@ -510,7 +545,7 @@ test("an explicit unknown permission pauses in memory until the host unblocks",
510
545
  "unknown state is not a persistent device block"
511
546
  );
512
547
  await traffic.flush({ reason: "permission_unknown" });
513
- assert.equal(requests.filter((request) => request.url === "/api/cms/system/traffic/visits/batch").length, 0);
548
+ assert.equal(requests.filter((request) => request.url === "https://cms.jx3box.com/api/cms/system/traffic/visits/batch").length, 0);
514
549
 
515
550
  assert.deepEqual(traffic.unblock(), { known: true, allow: true });
516
551
  assert.equal(traffic.getState().analytics.queue.blocked, false);
@@ -787,7 +822,7 @@ test("a trusted server ACK block is persisted before the queue is cleared", asyn
787
822
  const runtime = createRuntime({
788
823
  localStorage,
789
824
  async fetch(url, options) {
790
- if (String(url).startsWith("/api/cms/system/traffic/config")) {
825
+ if (String(url).startsWith("https://cms.jx3box.com/api/cms/system/traffic/config")) {
791
826
  return createResponse(200, {
792
827
  code: 0,
793
828
  data: {
@@ -854,7 +889,7 @@ test("the Traffic-only Journal never consumes or rewrites a shared Tracking Jour
854
889
  runtime: createRuntime({
855
890
  localStorage,
856
891
  async fetch(url, options) {
857
- if (String(url).startsWith("/api/cms/system/traffic/config")) {
892
+ if (String(url).startsWith("https://cms.jx3box.com/api/cms/system/traffic/config")) {
858
893
  return createResponse(200, {
859
894
  code: 0,
860
895
  data: {