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