@jx3box/jx3box-ui 2.4.2 → 2.4.4
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/assets/css/header/box.less +33 -3
- package/docs/agents/traffic-analytics.md +50 -0
- package/index.js +2 -0
- package/package.json +3 -2
- package/src/CommonHeader.vue +28 -2
- package/src/header/box-menu.js +42 -0
- package/src/header/box.vue +22 -20
- package/src/header/box2.vue +80 -264
- package/src/utils/client-stat.js +34 -6
- package/src/utils/traffic-analytics.js +504 -0
- package/tests/client-stat.test.js +11 -4
- package/tests/traffic-analytics-contract.test.js +907 -0
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
.db;
|
|
72
72
|
.auto(x);
|
|
73
73
|
fill: @color;
|
|
74
|
-
.tm(0.7);
|
|
75
74
|
}
|
|
76
75
|
.u-txt {
|
|
77
76
|
.fz(12px, 2);
|
|
@@ -134,11 +133,17 @@
|
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
@media screen and (max-width: @phone) {
|
|
136
|
+
.c-header-jx3box {
|
|
137
|
+
padding-top: 16px;
|
|
138
|
+
background: #1d2128;
|
|
139
|
+
box-shadow: inset 0 1px rgba(255, 255, 255, 0.04), 0 8px 24px rgba(0, 0, 0, 0.28);
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
.c-jx3box {
|
|
138
143
|
height: calc(100vh - @header-height);
|
|
139
144
|
.u-list {
|
|
140
145
|
border-top: none;
|
|
141
|
-
padding: 0
|
|
146
|
+
padding: 0 16px 18px;
|
|
142
147
|
display: grid;
|
|
143
148
|
grid-template-columns: repeat(4, 1fr);
|
|
144
149
|
justify-content: center;
|
|
@@ -150,7 +155,7 @@
|
|
|
150
155
|
flex: 1;
|
|
151
156
|
}
|
|
152
157
|
li {
|
|
153
|
-
padding:
|
|
158
|
+
padding: 4px;
|
|
154
159
|
.mr(0);
|
|
155
160
|
.mb(0);
|
|
156
161
|
}
|
|
@@ -158,9 +163,34 @@
|
|
|
158
163
|
.u-item {
|
|
159
164
|
.size(68px);
|
|
160
165
|
.auto(x);
|
|
166
|
+
padding: 7px;
|
|
167
|
+
border-radius: 10px;
|
|
168
|
+
border-color: rgba(255, 255, 255, 0.08);
|
|
169
|
+
background: #292e37;
|
|
170
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
|
|
171
|
+
transition: border-color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
|
|
172
|
+
|
|
161
173
|
.u-pic {
|
|
162
174
|
.size(28px);
|
|
163
175
|
}
|
|
176
|
+
|
|
177
|
+
.u-txt {
|
|
178
|
+
color: #d4d7de;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
.u-item:not(.u-doing):hover,
|
|
182
|
+
.u-item:not(.u-doing):active {
|
|
183
|
+
border-color: fade(@v4primary500, 55%);
|
|
184
|
+
background: fade(@v4primary500, 16%);
|
|
185
|
+
transform: translateY(-1px);
|
|
186
|
+
}
|
|
187
|
+
.u-close {
|
|
188
|
+
margin: 6px 16px 16px 0;
|
|
189
|
+
padding: 7px 12px;
|
|
190
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
background: rgba(255, 255, 255, 0.06);
|
|
193
|
+
color: #d4d7de;
|
|
164
194
|
}
|
|
165
195
|
.u-app-start {
|
|
166
196
|
clear: none;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Traffic 路径采集适配
|
|
2
|
+
|
|
3
|
+
`jx3box-ui` 只提供无渲染、显式启用的适配器,不会在安装组件库或渲染 `CommonHeader` 时自动采集。
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { createJx3boxTrafficAnalytics } from "@jx3box/jx3box-ui";
|
|
7
|
+
|
|
8
|
+
const traffic = createJx3boxTrafficAnalytics({
|
|
9
|
+
router,
|
|
10
|
+
project: "index",
|
|
11
|
+
client: "pc_web",
|
|
12
|
+
surface: "pc_web",
|
|
13
|
+
gameClient: "std",
|
|
14
|
+
// 传 heartbeat 的实时结果。未知态失败关闭,不发送。
|
|
15
|
+
resolveTrafficPermission: () => heartbeatState,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 应在 router 已建立,并取得 heartbeat 权限后显式执行。
|
|
19
|
+
await traffic.init();
|
|
20
|
+
|
|
21
|
+
// heartbeat 后续发现机器人或关闭采集时,立即持久阻断并清空队列。
|
|
22
|
+
traffic.setTrafficPermission({
|
|
23
|
+
traffic_allowed: false,
|
|
24
|
+
collection_blocked_reason: "robot",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// heartbeat 明确恢复后才解除持久阻断。
|
|
28
|
+
traffic.setTrafficPermission({ traffic_allowed: true });
|
|
29
|
+
|
|
30
|
+
// 同义的显式生命周期接口,供宿主的隐私开关直接调用。
|
|
31
|
+
traffic.block("host_privacy_disabled");
|
|
32
|
+
traffic.unblock();
|
|
33
|
+
|
|
34
|
+
// 宿主卸载时释放唯一 Router owner、pagehide 监听和队列请求。
|
|
35
|
+
traffic.destroy();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
适配器只创建一个 Analytics Core、一个 Router owner 和一个 `TrafficSink`。它不会创建 `TrackingSink`,也不会安装点击、曝光、热力图指令,因此一次路由变化只产生一个 canonical `page_view`。已经由宿主启用 Tracking 的应用不能再初始化本适配器;这类应用应直接用 `jx3box-common@9.5.2` 的组合 sink 创建唯一 Core/Router owner。
|
|
39
|
+
|
|
40
|
+
运行时规则只能来自 `/api/cms/system/traffic/config`。配置缺失、请求失败、规则字段不完整或动态参数没有 named validator 时不建立页面事件,也不使用本地放行规则。heartbeat 权限未知时会进入内存级 fail-closed:不建立新的 Journal 事件、取消进行中的请求;已经完成脱敏的 Journal 项会保留,直到明确允许后再恢复。宿主应优先等 heartbeat 给出明确权限后再调用 `init()`。批量请求发往 `/api/cms/system/traffic/visits/batch`,依赖逐 `event_id` ACK;409、429、网络错误和缺失 ACK 都保留原事件重试。
|
|
41
|
+
|
|
42
|
+
Config 请求与 Traffic envelope 共用同一个 recipient domain。`pc_web` 保留小写真实 hostname(由后端 allowlist 决定是否启用);`app`、`miniprogram`、`pc_game`、`mobile_game` 统一使用 `embedded`,不会把 WebView 的 `localhost` 等内部地址发给规则接口。
|
|
43
|
+
|
|
44
|
+
`traffic_allowed=false`、`is_robot=true`、`traffic.block(reason)` 或可信 `blocked` 会写入 `jx3box:analytics:traffic:block:v1`,并调用 `block({ clear: true, cancelInflight: true })`。下次初始化先恢复阻断,再接入 Router。只有 heartbeat 明确返回允许,或宿主显式调用 `setTrafficPermission({ traffic_allowed: true })` / `unblock()` 才解除。
|
|
45
|
+
|
|
46
|
+
宿主可通过 `beforeFlush(context)` 在每次网络发送前追加权限判断;返回 `false`/`{ allow: false }` 会保留队列并拒绝发送,返回可信 `block` 会持久阻断。`clear(reason)` 与 `cancelInflight(reason)` 分别清除安全 Journal 和取消当前请求。`init()` 幂等占有唯一 Router owner,`destroy()` 释放 Router/pagehide 监听并取消请求;销毁后不能再次初始化。
|
|
47
|
+
|
|
48
|
+
Traffic-only 队列使用独立的 `jx3box:analytics:traffic:queue:v1`,不会读取或改写公共 `jx3box:analytics:queue:v1` 中的 Tracking/multi-sink Journal。队列仍把无 delivery ownership 的旧数据视为 `tracking`,不会误发到 Traffic。
|
|
49
|
+
|
|
50
|
+
后端协议已提供 Traffic config 和逐 ID ACK。本适配器当前通过 9.5.2 fixture 验证;若目标环境仍是旧部署、响应缺字段或网络不可用,会安全保留或拒绝事件。构建与 fixture 测试不代表线上部署及真实链路已经验证。
|
package/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
setJx3boxUiLocale,
|
|
44
44
|
} from "./i18n";
|
|
45
45
|
import i18nMixin from "./i18n/mixin";
|
|
46
|
+
import { createJx3boxTrafficAnalytics } from "./src/utils/traffic-analytics";
|
|
46
47
|
|
|
47
48
|
const components = {
|
|
48
49
|
CommonHeader,
|
|
@@ -93,4 +94,5 @@ export {
|
|
|
93
94
|
mergeJx3boxUiMessages,
|
|
94
95
|
getJx3boxUiAvailableLocales,
|
|
95
96
|
setJx3boxUiLocale,
|
|
97
|
+
createJx3boxTrafficAnalytics,
|
|
96
98
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jx3box/jx3box-ui",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
4
4
|
"description": "JX3BOX Vue3 UI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"build:dev": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vue-cli-service build --mode development",
|
|
14
14
|
"build:prod": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vue-cli-service build --mode production",
|
|
15
15
|
"lint": "vue-cli-service lint",
|
|
16
|
+
"test:traffic": "node --test tests/traffic-analytics-contract.test.js",
|
|
16
17
|
"test:i18n": "node --test tests/wiki-i18n-contract.test.js",
|
|
17
18
|
"update": "npm --registry https://registry.npmjs.org install @jx3box/jx3box-common@latest @jx3box/jx3box-data@latest @jx3box/jx3box-editor@latest @jx3box/jx3box-emotion@latest @jx3box/jx3box-macro@latest @jx3box/jx3box-talent@latest"
|
|
18
19
|
},
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@element-plus/icons-vue": "^2.3.2",
|
|
36
|
-
"@jx3box/jx3box-common": "^9.
|
|
37
|
+
"@jx3box/jx3box-common": "^9.5.5",
|
|
37
38
|
"@jx3box/jx3box-data": "^3.9.6",
|
|
38
39
|
"@jx3box/jx3box-editor": "^3.2.13",
|
|
39
40
|
"@jx3box/jx3box-emotion": "^1.3.2",
|
package/src/CommonHeader.vue
CHANGED
|
@@ -85,7 +85,14 @@ import miniprogram from "@jx3box/jx3box-common/data/miniprogram.json";
|
|
|
85
85
|
import { getGlobalConfig } from "../service/header";
|
|
86
86
|
import { getConfig, getMyAccountStatus } from "../service/cms";
|
|
87
87
|
import { clearActiveAuthToken, refreshTokenIfNeeded } from "./utils/auth-token-refresh";
|
|
88
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
checkClientStatOnVisible,
|
|
90
|
+
ensureClientInstanceId,
|
|
91
|
+
installClientStatReporting,
|
|
92
|
+
resolveCurrentClientSurface,
|
|
93
|
+
resolveClientTrafficPermission,
|
|
94
|
+
} from "./utils/client-stat";
|
|
95
|
+
import { installCommonHeaderTrafficAnalytics } from "./utils/traffic-analytics";
|
|
89
96
|
import User from "@jx3box/jx3box-common/js/user.js";
|
|
90
97
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
91
98
|
|
|
@@ -105,7 +112,10 @@ export default {
|
|
|
105
112
|
"header-box": box,
|
|
106
113
|
"header-box2": box2,
|
|
107
114
|
},
|
|
108
|
-
props:
|
|
115
|
+
props: {
|
|
116
|
+
overlayEnable: Boolean,
|
|
117
|
+
trafficEnabled: { type: Boolean, default: true },
|
|
118
|
+
},
|
|
109
119
|
data: function () {
|
|
110
120
|
return {
|
|
111
121
|
isOverlay: false,
|
|
@@ -449,6 +459,22 @@ export default {
|
|
|
449
459
|
created: function () {
|
|
450
460
|
this.init();
|
|
451
461
|
installClientStatReporting();
|
|
462
|
+
if (this.trafficEnabled) {
|
|
463
|
+
try {
|
|
464
|
+
installCommonHeaderTrafficAnalytics({
|
|
465
|
+
router: this.$router,
|
|
466
|
+
runtime: window,
|
|
467
|
+
surface: resolveCurrentClientSurface(),
|
|
468
|
+
instanceId: ensureClientInstanceId(),
|
|
469
|
+
resolveTrafficPermission: resolveClientTrafficPermission,
|
|
470
|
+
webVersion: window.__APP_VERSION__ || process.env.VUE_APP_VERSION || process.env.VITE_APP_VERSION || null,
|
|
471
|
+
}).catch(function () {
|
|
472
|
+
// Traffic is fail-closed and must never affect the shared header or host page.
|
|
473
|
+
});
|
|
474
|
+
} catch (error) {
|
|
475
|
+
// Storage/privacy failures must not affect the shared header or host page.
|
|
476
|
+
}
|
|
477
|
+
}
|
|
452
478
|
window.addEventListener("resize", this.updateScreen, { passive: true });
|
|
453
479
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
454
480
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { flatten } from "lodash";
|
|
3
|
+
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
4
|
+
|
|
5
|
+
const BOX_MENU_CACHE_KEY = "jx3box:box-menu:app-v1";
|
|
6
|
+
|
|
7
|
+
function readCache() {
|
|
8
|
+
try {
|
|
9
|
+
const data = JSON.parse(sessionStorage.getItem(BOX_MENU_CACHE_KEY));
|
|
10
|
+
return Array.isArray(data) && data.length ? data : null;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeCache(data) {
|
|
17
|
+
try {
|
|
18
|
+
sessionStorage.setItem(BOX_MENU_CACHE_KEY, JSON.stringify(data));
|
|
19
|
+
} catch (e) {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function loadBoxMenu(fallback = []) {
|
|
23
|
+
const cached = readCache();
|
|
24
|
+
if (cached) return Promise.resolve(cached);
|
|
25
|
+
|
|
26
|
+
return axios
|
|
27
|
+
.get(JX3BOX.__imgPath + "logo/app.json")
|
|
28
|
+
.then((res) => {
|
|
29
|
+
const source = flatten(Object.values(res.data || {}));
|
|
30
|
+
const data = source.map((item) => ({
|
|
31
|
+
img: item.key + ".svg",
|
|
32
|
+
uuid: item.key,
|
|
33
|
+
abbr: item.label,
|
|
34
|
+
href: item.link,
|
|
35
|
+
client: item.client || ["std", "origin"],
|
|
36
|
+
}));
|
|
37
|
+
const result = data.length ? data : fallback;
|
|
38
|
+
writeCache(result);
|
|
39
|
+
return result;
|
|
40
|
+
})
|
|
41
|
+
.catch(() => fallback);
|
|
42
|
+
}
|
package/src/header/box.vue
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<li v-for="(item, i) in list" :key="i" :class="{ 'u-app-start': item.lf }">
|
|
16
16
|
<a class="u-item" :href="item.href" :target="getTarget(item.href)">
|
|
17
17
|
<img class="u-pic" :src="getBoxIcon(item.img)" />
|
|
18
|
-
<span class="u-txt">{{ item.abbr }}</span>
|
|
18
|
+
<span class="u-txt" :title="item.abbr">{{ getDisplayName(item.abbr) }}</span>
|
|
19
19
|
</a>
|
|
20
20
|
</li>
|
|
21
21
|
</ul>
|
|
@@ -33,8 +33,8 @@ import Bus from "../../utils/bus";
|
|
|
33
33
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
34
34
|
import i18nMixin from "../../i18n/mixin";
|
|
35
35
|
import box from "../../assets/data/box.json";
|
|
36
|
-
import { getMenu } from "../../service/header.js";
|
|
37
36
|
import { trimSlash } from "./utils";
|
|
37
|
+
import { loadBoxMenu } from "./box-menu";
|
|
38
38
|
|
|
39
39
|
const { __imgPath, __cdn } = JX3BOX;
|
|
40
40
|
export default {
|
|
@@ -56,7 +56,7 @@ export default {
|
|
|
56
56
|
},
|
|
57
57
|
computed: {
|
|
58
58
|
homeicon: function () {
|
|
59
|
-
return
|
|
59
|
+
return __cdn + "logo/logo-dark/home.svg";
|
|
60
60
|
},
|
|
61
61
|
homeicon_hover: function () {
|
|
62
62
|
return __imgPath + "image/box/home_on.svg";
|
|
@@ -66,7 +66,8 @@ export default {
|
|
|
66
66
|
},
|
|
67
67
|
list: function () {
|
|
68
68
|
return this.data.filter((item) => {
|
|
69
|
-
|
|
69
|
+
const visible = item.status == null || !!item.status;
|
|
70
|
+
return visible && this.matchedClient(item.client);
|
|
70
71
|
});
|
|
71
72
|
},
|
|
72
73
|
prefix: function () {
|
|
@@ -78,13 +79,25 @@ export default {
|
|
|
78
79
|
Bus.emit("toggleBox", false);
|
|
79
80
|
},
|
|
80
81
|
matchedClient: function (client) {
|
|
81
|
-
|
|
82
|
+
const clients = Array.isArray(client)
|
|
83
|
+
? client
|
|
84
|
+
: String(client || "")
|
|
85
|
+
.split(",")
|
|
86
|
+
.map((item) => item.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
return !clients.length || clients.includes("all") || clients.includes(this.client);
|
|
82
89
|
},
|
|
83
90
|
getBoxIcon: function (val) {
|
|
84
91
|
val = val && val?.replace(".png", ".svg");
|
|
85
|
-
let web_url = __cdn + "logo/logo-
|
|
92
|
+
let web_url = __cdn + "logo/logo-dark/" + val;
|
|
86
93
|
return web_url;
|
|
87
94
|
},
|
|
95
|
+
getDisplayName: function (name) {
|
|
96
|
+
const chars = Array.from(String(name || "").trim());
|
|
97
|
+
if (chars.length === 5) return chars.slice(0, 3).join("");
|
|
98
|
+
if (chars.length === 4) return chars.slice(0, 2).join("");
|
|
99
|
+
return chars.join("");
|
|
100
|
+
},
|
|
88
101
|
getTarget: function (val) {
|
|
89
102
|
if (window.innerWidth < 768 || val?.startsWith("/")) {
|
|
90
103
|
return "_self";
|
|
@@ -93,20 +106,9 @@ export default {
|
|
|
93
106
|
}
|
|
94
107
|
},
|
|
95
108
|
loadMenu() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.data = _box;
|
|
100
|
-
} else {
|
|
101
|
-
getMenu("box").then((res) => {
|
|
102
|
-
this.data = res.data?.data?.val;
|
|
103
|
-
sessionStorage.setItem("box", JSON.stringify(this.data));
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
} catch (e) {
|
|
107
|
-
this.data = box;
|
|
108
|
-
console.log("loadBox error", e);
|
|
109
|
-
}
|
|
109
|
+
loadBoxMenu(box).then((data) => {
|
|
110
|
+
this.data = data;
|
|
111
|
+
});
|
|
110
112
|
},
|
|
111
113
|
trimSlash(link) {
|
|
112
114
|
return trimSlash(`${this.prefix}:${link}`);
|