@jx3box/jx3box-ui 2.3.30 → 2.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/docs/agents/client-stat.md +16 -0
- package/package.json +1 -1
- package/src/CommonHeader.vue +3 -0
- package/src/utils/client-stat.js +272 -0
- package/tests/client-stat.test.js +48 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# PC 公共头客户端统计
|
|
2
|
+
|
|
3
|
+
`CommonHeader` 为引用公共头的普通 PC 项目接入新版客户端实例 heartbeat。
|
|
4
|
+
|
|
5
|
+
- 接口:`POST /api/cms/system/stat/heartbeat`。
|
|
6
|
+
- SDK 版本:当前固定为 `v0.0.2`,只在统计组件协议升级时调整。
|
|
7
|
+
- 实例 ID:复用本地 `jx3box:device_id` 随机 UUID;不使用硬件指纹。
|
|
8
|
+
- 普通桌面浏览器为 `client=pc_web`;手机浏览器访问 PC 页面为 `client=mobile_web`。
|
|
9
|
+
- 小程序和 App 容器按统一客户端枚举识别;游戏内页面的 `pc_game/mobile_game` 由对应页面以后主动指定,不在公共头推断。
|
|
10
|
+
- 通用客户端识别与 JX3BOX 业务规则分离:`src/utils/client-stat-business.js` 负责项目专属覆盖;UA 包含 `jianghudaily` 时不区分大小写地上报 `mobile_game`,以后抽公共包时以业务适配器注入。
|
|
11
|
+
- UID 不进入 payload,由 service-cms 从可选 JWT 中读取;IP 同样由服务端反代请求头读取。
|
|
12
|
+
- 生产环境启动后随机延迟 3~15 秒,同一北京时间日期、UID 和 Web 版本只成功上报一次。
|
|
13
|
+
- Web 版本按 `window.__APP_VERSION__`、`process.env.VUE_APP_VERSION`、`process.env.VITE_APP_VERSION` 依次降级读取;均不存在时使用 `unknown` 参与本地签名,并省略 heartbeat payload 中的 `web_version`。
|
|
14
|
+
- 页面恢复可见时补检;请求失败不写成功标记,也不影响公共头和宿主页面。
|
|
15
|
+
- localhost 每次公共头重新创建都强制上报,方便联调。
|
|
16
|
+
- UA 命中 ArkWeb/HarmonyOS/OpenHarmony 时识别为 `platform=harmony`;明确的 `Phone/Tablet; OpenHarmony x.x` 会在上报前规范为 `os_name=OpenHarmony`、对应版本和设备类型,不保留设备型号或完整固件展示串。
|
package/package.json
CHANGED
package/src/CommonHeader.vue
CHANGED
|
@@ -85,6 +85,7 @@ 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 { checkClientStatOnVisible, installClientStatReporting } from "./utils/client-stat";
|
|
88
89
|
import User from "@jx3box/jx3box-common/js/user.js";
|
|
89
90
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
90
91
|
|
|
@@ -262,6 +263,7 @@ export default {
|
|
|
262
263
|
handleVisibilityChange: function () {
|
|
263
264
|
if (document.visibilityState === "visible") {
|
|
264
265
|
this.refreshAuthToken();
|
|
266
|
+
checkClientStatOnVisible();
|
|
265
267
|
}
|
|
266
268
|
},
|
|
267
269
|
|
|
@@ -446,6 +448,7 @@ export default {
|
|
|
446
448
|
},
|
|
447
449
|
created: function () {
|
|
448
450
|
this.init();
|
|
451
|
+
installClientStatReporting();
|
|
449
452
|
window.addEventListener("resize", this.updateScreen, { passive: true });
|
|
450
453
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
451
454
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { $cms } from "@jx3box/jx3box-common/js/api";
|
|
2
|
+
import { isMiniProgram, isApp } from "@jx3box/jx3box-common/js/utils";
|
|
3
|
+
import User from "@jx3box/jx3box-common/js/user";
|
|
4
|
+
import { applyJx3boxClientStatRules } from "./client-stat-business";
|
|
5
|
+
|
|
6
|
+
const INSTANCE_ID_KEY = "jx3box:device_id";
|
|
7
|
+
const REPORT_DATE_KEY = "jx3box:client-stat-report-date";
|
|
8
|
+
const REPORT_SIGNATURE_KEY = "jx3box:client-stat-report-signature";
|
|
9
|
+
const STAT_SDK_VERSION = "v0.0.2";
|
|
10
|
+
const STARTUP_DELAY_MIN = 3 * 1000;
|
|
11
|
+
const STARTUP_DELAY_MAX = 15 * 1000;
|
|
12
|
+
|
|
13
|
+
let reportTask = null;
|
|
14
|
+
let scheduledTimer = null;
|
|
15
|
+
|
|
16
|
+
function compactPayload(payload) {
|
|
17
|
+
return Object.fromEntries(
|
|
18
|
+
Object.entries(payload).filter(([, value]) => value !== undefined && value !== null && value !== "")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rangedNumber(value, min, max, { integer = false } = {}) {
|
|
23
|
+
if (value === undefined || value === null || value === "") return null;
|
|
24
|
+
const result = Number(value);
|
|
25
|
+
if (!Number.isFinite(result) || result < min || result > max) return null;
|
|
26
|
+
if (integer && !Number.isInteger(result)) return null;
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createInstanceId() {
|
|
31
|
+
const browserCrypto = window.crypto;
|
|
32
|
+
if (typeof browserCrypto?.randomUUID === "function") return browserCrypto.randomUUID();
|
|
33
|
+
|
|
34
|
+
const bytes = new Uint8Array(16);
|
|
35
|
+
browserCrypto.getRandomValues(bytes);
|
|
36
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
37
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
38
|
+
const value = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
39
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ensureClientInstanceId() {
|
|
43
|
+
const current = localStorage.getItem(INSTANCE_ID_KEY);
|
|
44
|
+
if (current) return current;
|
|
45
|
+
|
|
46
|
+
const instanceId = createInstanceId();
|
|
47
|
+
localStorage.setItem(INSTANCE_ID_KEY, instanceId);
|
|
48
|
+
return instanceId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function chinaDate(date = new Date()) {
|
|
52
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
53
|
+
timeZone: "Asia/Shanghai",
|
|
54
|
+
year: "numeric",
|
|
55
|
+
month: "2-digit",
|
|
56
|
+
day: "2-digit",
|
|
57
|
+
}).formatToParts(date);
|
|
58
|
+
const values = Object.fromEntries(parts.map((item) => [item.type, item.value]));
|
|
59
|
+
return `${values.year}-${values.month}-${values.day}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveChannel() {
|
|
63
|
+
const sources = [window.location.search, String(window.location.hash || "").split("?")[1]];
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
if (!source) continue;
|
|
66
|
+
const value = new URLSearchParams(String(source).replace(/^\?/, "")).get("__from");
|
|
67
|
+
if (value?.trim()) return value.trim().slice(0, 64);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolvePlatform(userAgent, navigatorPlatform) {
|
|
73
|
+
if (/ArkWeb|HarmonyOS|OpenHarmony/i.test(userAgent)) return "harmony";
|
|
74
|
+
if (/iPhone|iPad|iPod/i.test(userAgent)) return "ios";
|
|
75
|
+
if (/Android/i.test(userAgent)) return "android";
|
|
76
|
+
if (/Windows/i.test(userAgent)) return "windows";
|
|
77
|
+
if (/Macintosh|MacIntel/i.test(userAgent) || navigatorPlatform === "MacIntel") return "macos";
|
|
78
|
+
if (/Linux/i.test(userAgent)) return "linux";
|
|
79
|
+
return "unknown";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeHarmonySystem(platform, userAgent) {
|
|
83
|
+
if (platform !== "harmony") return { os_name: platform, os_version: null };
|
|
84
|
+
const openHarmony = String(userAgent || "").match(/\b(?:Phone|Tablet);\s*OpenHarmony\s+([0-9]+(?:\.[0-9]+)*)\b/i);
|
|
85
|
+
if (openHarmony) return { os_name: "OpenHarmony", os_version: openHarmony[1] };
|
|
86
|
+
return {
|
|
87
|
+
os_name: /HarmonyOS/i.test(userAgent) ? "HarmonyOS" : "OpenHarmony",
|
|
88
|
+
os_version: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveClient(platform, userAgent) {
|
|
93
|
+
if (isApp()) return "app";
|
|
94
|
+
if (isMiniProgram() || /miniprogram/i.test(userAgent)) return "miniprogram";
|
|
95
|
+
if (["ios", "android"].includes(platform) || /Mobile|Tablet/i.test(userAgent)) return "mobile_web";
|
|
96
|
+
if (["windows", "macos", "linux"].includes(platform)) return "pc_web";
|
|
97
|
+
return "unknown";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseBrowser(userAgent) {
|
|
101
|
+
const candidates = [
|
|
102
|
+
["Edge", /EdgA?[\s/]([\d.]+)/i],
|
|
103
|
+
["Chrome", /(?:Chrome|CriOS)[\s/]([\d.]+)/i],
|
|
104
|
+
["Firefox", /(?:Firefox|FxiOS)[\s/]([\d.]+)/i],
|
|
105
|
+
["Safari", /Version[\s/]([\d.]+).*Safari/i],
|
|
106
|
+
];
|
|
107
|
+
const matched = candidates.find(([, pattern]) => pattern.test(userAgent));
|
|
108
|
+
const browserMatch = matched?.[1]?.exec(userAgent);
|
|
109
|
+
const webkit = /AppleWebKit[\s/]([\d.]+)/i.exec(userAgent);
|
|
110
|
+
const chromium = /(?:Chrome|Chromium|CriOS)[\s/]([\d.]+)/i.exec(userAgent);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
browser_name: matched?.[0] || null,
|
|
114
|
+
browser_version: browserMatch?.[1] || null,
|
|
115
|
+
engine_name: chromium ? "Blink" : webkit ? "WebKit" : null,
|
|
116
|
+
webview_version: /;\s*wv\)|Version\/4\.0/i.test(userAgent)
|
|
117
|
+
? browserMatch?.[1] || chromium?.[1] || webkit?.[1] || null
|
|
118
|
+
: null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function supportsWebGl() {
|
|
123
|
+
try {
|
|
124
|
+
const canvas = document.createElement("canvas");
|
|
125
|
+
return !!(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"));
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveOrientation() {
|
|
132
|
+
const type = String(window.screen?.orientation?.type || "");
|
|
133
|
+
if (type.startsWith("portrait")) return "portrait";
|
|
134
|
+
if (type.startsWith("landscape")) return "landscape";
|
|
135
|
+
return window.innerWidth > window.innerHeight ? "landscape" : "portrait";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveDisplayMode() {
|
|
139
|
+
if (document.fullscreenElement) return "fullscreen";
|
|
140
|
+
if (window.matchMedia?.("(display-mode: standalone)")?.matches) return "standalone";
|
|
141
|
+
return "browser";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveWebVersion() {
|
|
145
|
+
return window.__APP_VERSION__ || process.env.VUE_APP_VERSION || process.env.VITE_APP_VERSION || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildClientStatPayload(instanceId) {
|
|
149
|
+
const nav = window.navigator || {};
|
|
150
|
+
const screen = window.screen || {};
|
|
151
|
+
const connection = nav.connection || nav.mozConnection || nav.webkitConnection || {};
|
|
152
|
+
const userAgent = String(nav.userAgent || "");
|
|
153
|
+
const platform = resolvePlatform(userAgent, nav.platform);
|
|
154
|
+
const context = applyJx3boxClientStatRules(
|
|
155
|
+
{ platform, client: resolveClient(platform, userAgent) },
|
|
156
|
+
{ userAgent }
|
|
157
|
+
);
|
|
158
|
+
const browser = parseBrowser(userAgent);
|
|
159
|
+
const system = normalizeHarmonySystem(context.platform, userAgent);
|
|
160
|
+
|
|
161
|
+
return compactPayload({
|
|
162
|
+
instance_id: instanceId,
|
|
163
|
+
product: "jx3box",
|
|
164
|
+
client: context.client,
|
|
165
|
+
platform: context.platform,
|
|
166
|
+
domain: window.location.hostname,
|
|
167
|
+
channel: resolveChannel(),
|
|
168
|
+
web_version: resolveWebVersion(),
|
|
169
|
+
sdk_version: STAT_SDK_VERSION,
|
|
170
|
+
|
|
171
|
+
os_name: system.os_name,
|
|
172
|
+
os_version: system.os_version,
|
|
173
|
+
device_type: ["ios", "android"].includes(platform) ? (/iPad|Tablet/i.test(userAgent) ? "tablet" : "phone") : "desktop",
|
|
174
|
+
architecture: /arm64|aarch64/i.test(userAgent)
|
|
175
|
+
? "arm64"
|
|
176
|
+
: /x86_64|x64|Win64|x86-64/i.test(userAgent)
|
|
177
|
+
? "x64"
|
|
178
|
+
: null,
|
|
179
|
+
hardware_concurrency: rangedNumber(nav.hardwareConcurrency, 1, 1024, { integer: true }),
|
|
180
|
+
device_memory_gb: rangedNumber(nav.deviceMemory, 0.1, 655.35),
|
|
181
|
+
|
|
182
|
+
screen_width_css: rangedNumber(screen.width, 1, 100000, { integer: true }),
|
|
183
|
+
screen_height_css: rangedNumber(screen.height, 1, 100000, { integer: true }),
|
|
184
|
+
screen_avail_width_css: rangedNumber(screen.availWidth, 1, 100000, { integer: true }),
|
|
185
|
+
screen_avail_height_css: rangedNumber(screen.availHeight, 1, 100000, { integer: true }),
|
|
186
|
+
viewport_width_css: rangedNumber(window.innerWidth, 1, 100000, { integer: true }),
|
|
187
|
+
viewport_height_css: rangedNumber(window.innerHeight, 1, 100000, { integer: true }),
|
|
188
|
+
device_pixel_ratio: rangedNumber(window.devicePixelRatio, 0.1, 20),
|
|
189
|
+
color_depth: rangedNumber(screen.colorDepth, 1, 128, { integer: true }),
|
|
190
|
+
orientation: resolveOrientation(),
|
|
191
|
+
display_mode: resolveDisplayMode(),
|
|
192
|
+
prefers_dark: !!window.matchMedia?.("(prefers-color-scheme: dark)")?.matches,
|
|
193
|
+
prefers_reduced_motion: !!window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches,
|
|
194
|
+
|
|
195
|
+
...browser,
|
|
196
|
+
language: nav.languages?.find(Boolean) || nav.language || null,
|
|
197
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null,
|
|
198
|
+
touch_points: rangedNumber(nav.maxTouchPoints, 0, 100, { integer: true }),
|
|
199
|
+
capabilities: {
|
|
200
|
+
wasm: typeof WebAssembly !== "undefined",
|
|
201
|
+
service_worker: "serviceWorker" in nav,
|
|
202
|
+
webgl: supportsWebGl(),
|
|
203
|
+
},
|
|
204
|
+
network_type: String(connection.type || "unknown").toLowerCase(),
|
|
205
|
+
network_effective_type: String(connection.effectiveType || "unknown").toLowerCase(),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getReportSignature() {
|
|
210
|
+
const uid = User.isLogin() ? Number(User.getInfo()?.uid || localStorage.getItem("uid") || 0) : 0;
|
|
211
|
+
return `${uid}:${resolveWebVersion() || "unknown"}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function reportClientStat({ force = false } = {}) {
|
|
215
|
+
if (reportTask) return reportTask;
|
|
216
|
+
|
|
217
|
+
reportTask = (async () => {
|
|
218
|
+
const today = chinaDate();
|
|
219
|
+
const signature = getReportSignature();
|
|
220
|
+
if (
|
|
221
|
+
!force &&
|
|
222
|
+
localStorage.getItem(REPORT_DATE_KEY) === today &&
|
|
223
|
+
localStorage.getItem(REPORT_SIGNATURE_KEY) === signature
|
|
224
|
+
) {
|
|
225
|
+
return { reported: false, reason: "already_reported" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const payload = buildClientStatPayload(ensureClientInstanceId());
|
|
229
|
+
const response = await $cms({ mute: true }).post("/api/cms/system/stat/heartbeat", payload, {
|
|
230
|
+
timeout: 10000,
|
|
231
|
+
skipForbiddenSessionClear: true,
|
|
232
|
+
});
|
|
233
|
+
localStorage.setItem(REPORT_DATE_KEY, today);
|
|
234
|
+
localStorage.setItem(REPORT_SIGNATURE_KEY, signature);
|
|
235
|
+
return { reported: true, data: response?.data?.data || null, payload };
|
|
236
|
+
})().finally(() => {
|
|
237
|
+
reportTask = null;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return reportTask;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isLocalhost() {
|
|
244
|
+
return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function startupDelay() {
|
|
248
|
+
if (process.env.NODE_ENV !== "production" || isLocalhost()) return 0;
|
|
249
|
+
return Math.floor(STARTUP_DELAY_MIN + Math.random() * (STARTUP_DELAY_MAX - STARTUP_DELAY_MIN));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function scheduleClientStatReport({ force = false, delay = startupDelay() } = {}) {
|
|
253
|
+
if (scheduledTimer) {
|
|
254
|
+
if (!force) return;
|
|
255
|
+
window.clearTimeout(scheduledTimer);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
scheduledTimer = window.setTimeout(() => {
|
|
259
|
+
scheduledTimer = null;
|
|
260
|
+
reportClientStat({ force }).catch(() => {
|
|
261
|
+
// 统计失败不影响公共头和宿主页面;未写成功标记时,下次恢复可见会继续尝试。
|
|
262
|
+
});
|
|
263
|
+
}, delay);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function installClientStatReporting() {
|
|
267
|
+
scheduleClientStatReport({ force: isLocalhost() });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function checkClientStatOnVisible() {
|
|
271
|
+
scheduleClientStatReport();
|
|
272
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const root = path.resolve(__dirname, "..");
|
|
5
|
+
const commonHeader = fs.readFileSync(path.join(root, "src/CommonHeader.vue"), "utf8");
|
|
6
|
+
const clientStat = fs.readFileSync(path.join(root, "src/utils/client-stat.js"), "utf8");
|
|
7
|
+
const clientStatBusiness = fs.readFileSync(path.join(root, "src/utils/client-stat-business.js"), "utf8");
|
|
8
|
+
|
|
9
|
+
function assert(condition, message) {
|
|
10
|
+
if (!condition) throw new Error(message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
assert(commonHeader.includes("installClientStatReporting()"), "CommonHeader should install client statistics");
|
|
14
|
+
assert(
|
|
15
|
+
commonHeader.includes("checkClientStatOnVisible()") && commonHeader.includes("visibilitychange"),
|
|
16
|
+
"CommonHeader should retry statistics when the page becomes visible"
|
|
17
|
+
);
|
|
18
|
+
assert(clientStat.includes('const INSTANCE_ID_KEY = "jx3box:device_id"'), "PC should reuse the client instance id key");
|
|
19
|
+
assert(clientStat.includes('product: "jx3box"'), "heartbeat should identify the JX3BOX product");
|
|
20
|
+
assert(clientStat.includes('const STAT_SDK_VERSION = "v0.0.2"'), "statistics SDK version should remain explicit");
|
|
21
|
+
assert(clientStat.includes('return "pc_web"'), "desktop browser traffic should report pc_web");
|
|
22
|
+
assert(clientStat.includes('return "mobile_web"'), "mobile browsers visiting PC pages should report mobile_web");
|
|
23
|
+
assert(
|
|
24
|
+
clientStat.includes("applyJx3boxClientStatRules"),
|
|
25
|
+
"generic client detection should pass through the JX3BOX business rule adapter"
|
|
26
|
+
);
|
|
27
|
+
assert(clientStatBusiness.includes("/jianghudaily/i"), "Jianghu Daily UA matching should be case-insensitive");
|
|
28
|
+
assert(clientStatBusiness.includes('client: "mobile_game"'), "Jianghu Daily should report mobile_game");
|
|
29
|
+
assert(
|
|
30
|
+
clientStat.includes("/ArkWeb|HarmonyOS|OpenHarmony/i") && clientStat.includes('os_name: "OpenHarmony"'),
|
|
31
|
+
"PC statistics should normalize OpenHarmony before reporting"
|
|
32
|
+
);
|
|
33
|
+
assert(
|
|
34
|
+
clientStat.includes("window.__APP_VERSION__ || process.env.VUE_APP_VERSION || process.env.VITE_APP_VERSION"),
|
|
35
|
+
"web version should use the shared app version fallback chain"
|
|
36
|
+
);
|
|
37
|
+
assert(!clientStat.includes("__JX3BOX_VERSION__"), "shared statistics must not depend on a JX3BOX-specific version global");
|
|
38
|
+
assert(!clientStat.includes("VUE_APP_BUILD_VERSION"), "web version should not use the deprecated build-version fallback");
|
|
39
|
+
assert(clientStat.includes("/api/cms/system/stat/heartbeat"), "heartbeat should use the new statistics endpoint");
|
|
40
|
+
assert(clientStat.includes("$cms({ mute: true })"), "heartbeat should reuse the authenticated CMS client");
|
|
41
|
+
assert(
|
|
42
|
+
clientStat.indexOf("localStorage.setItem(REPORT_DATE_KEY") > clientStat.indexOf("await $cms"),
|
|
43
|
+
"successful report markers must only be stored after the request resolves"
|
|
44
|
+
);
|
|
45
|
+
assert(!clientStat.includes("current_uid:"), "heartbeat payload must not report uid");
|
|
46
|
+
assert(!clientStat.includes("ip:"), "heartbeat payload must not report ip");
|
|
47
|
+
|
|
48
|
+
console.log("client statistics checks passed");
|