@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
|
@@ -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,
|
|
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: {
|