befly-shared 2.12.0 → 2.14.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.
@@ -0,0 +1,86 @@
1
+ import { createHttp } from "../utils/createHttp.js";
2
+
3
+ async function returnPayload(payload) {
4
+ return payload;
5
+ }
6
+
7
+ export function assertReportRequest(options, operation) {
8
+ if (!options || (typeof options.request !== "function" && typeof options.apiPath !== "string")) {
9
+ throw new Error(`${operation}.options 必须提供 request 函数或 apiPath 字符串`, {
10
+ cause: null,
11
+ code: "validation",
12
+ subsystem: "shared",
13
+ operation: operation
14
+ });
15
+ }
16
+ }
17
+
18
+ export function getReportRequest(options, operation) {
19
+ assertReportRequest(options, operation);
20
+
21
+ if (typeof options.request === "function") {
22
+ return options.request;
23
+ }
24
+
25
+ return createHttp({
26
+ apiPath: options.apiPath,
27
+ getToken: typeof options.getToken === "function" ? options.getToken : () => "",
28
+ onReturn: typeof options.onReturn === "function" ? options.onReturn : returnPayload
29
+ });
30
+ }
31
+
32
+ export function copyReportData(target, source) {
33
+ if (!source || typeof source !== "object") {
34
+ return;
35
+ }
36
+
37
+ for (const key of Object.keys(source)) {
38
+ if (source[key] !== undefined) {
39
+ target[key] = source[key];
40
+ }
41
+ }
42
+ }
43
+
44
+ export function createCommonReportData(options) {
45
+ return {
46
+ source: options.source || "",
47
+ productName: options.productName || "",
48
+ productCode: options.productCode || "",
49
+ productVersion: options.productVersion || ""
50
+ };
51
+ }
52
+
53
+ export function appendPageAndExtraData(data, options, extraData) {
54
+ if (typeof options.getPageData === "function") {
55
+ copyReportData(data, options.getPageData());
56
+ }
57
+
58
+ copyReportData(data, extraData);
59
+ return data;
60
+ }
61
+
62
+ export function getRouteReportData(route) {
63
+ return {
64
+ pagePath: route?.fullPath || route?.path || "",
65
+ pageName: String(route?.name || route?.meta?.title || route?.path || "")
66
+ };
67
+ }
68
+
69
+ export function getCurrentRouterData(router) {
70
+ const currentRoute = router?.currentRoute?.value || {};
71
+ let fallbackPath = "";
72
+ let fallbackTitle = "";
73
+
74
+ if (typeof globalThis.window !== "undefined") {
75
+ fallbackPath = globalThis.window.location.hash || globalThis.window.location.pathname || "";
76
+ }
77
+
78
+ if (typeof globalThis.document !== "undefined") {
79
+ fallbackTitle = globalThis.document.title || "";
80
+ }
81
+
82
+ return {
83
+ pagePath: String(currentRoute.fullPath || currentRoute.path || fallbackPath),
84
+ pageName: String(currentRoute.name || currentRoute.meta?.title || currentRoute.path || fallbackTitle)
85
+ };
86
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "befly-shared",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "gitHead": "70c48ac058875255ac8b54c5f4b5987b997c3bfd",
5
5
  "private": false,
6
6
  "description": "Befly Shared - 通用工具函数库",
7
7
  "files": [
8
8
  "utils/",
9
+ "internal/",
9
10
  "package.json"
10
11
  ],
11
12
  "type": "module",
@@ -2,7 +2,7 @@ export function createHttp(options) {
2
2
  const request = async function (url, data, dropValues = [], dropKeyValue = {}) {
3
3
  try {
4
4
  const fullUrl = /^https?:\/\//i.test(url) ? url : `${options.apiPath}${url}`;
5
- const isForm = data instanceof FormData;
5
+ const isForm = typeof FormData !== "undefined" && data instanceof FormData;
6
6
  const dropList = [null, undefined, ...dropValues];
7
7
  const dropKeyMap = {
8
8
  ...dropKeyValue,
@@ -0,0 +1,282 @@
1
+ import { appendPageAndExtraData, createCommonReportData, getReportRequest } from "../internal/reportUtil.js";
2
+
3
+ const ERROR_REPORT_PATH = "/core/tongJi/errorReport";
4
+ const ERROR_REPORT_DEDUP_GAP = 3000;
5
+ let fetchWrapped = false;
6
+
7
+ function buildReportData(options, errorType, message, detail, extraData) {
8
+ const data = createCommonReportData(options);
9
+ data.errorType = String(errorType || "unknown");
10
+ data.message = String(message || "未知错误");
11
+ data.detail = String(detail || "");
12
+
13
+ return appendPageAndExtraData(data, options, extraData);
14
+ }
15
+
16
+ export function getErrorMessage(error) {
17
+ if (typeof error === "string") {
18
+ return error;
19
+ }
20
+
21
+ if (error && typeof error.message === "string") {
22
+ return error.message;
23
+ }
24
+
25
+ return "未知错误";
26
+ }
27
+
28
+ export function getErrorDetail(error) {
29
+ if (!error) {
30
+ return "";
31
+ }
32
+
33
+ if (typeof error === "string") {
34
+ return error;
35
+ }
36
+
37
+ if (typeof error.stack === "string" && error.stack) {
38
+ return error.stack;
39
+ }
40
+
41
+ try {
42
+ return JSON.stringify(error);
43
+ } catch {
44
+ return String(error);
45
+ }
46
+ }
47
+
48
+ export function createErrorReport(options) {
49
+ const request = getReportRequest(options, "createErrorReport");
50
+
51
+ let reporting = false;
52
+ let lastErrorKey = "";
53
+ let lastErrorTime = 0;
54
+
55
+ function shouldSkipError(errorType, message, detail) {
56
+ const now = Date.now();
57
+ const key = `${errorType}|${message}|${detail}`;
58
+
59
+ if (key === lastErrorKey && now - lastErrorTime < ERROR_REPORT_DEDUP_GAP) {
60
+ return true;
61
+ }
62
+
63
+ lastErrorKey = key;
64
+ lastErrorTime = now;
65
+ return false;
66
+ }
67
+
68
+ return {
69
+ reportError: async function (errorType, message, detail, extraData = {}) {
70
+ const safeErrorType = String(errorType || "unknown");
71
+ const safeMessage = String(message || "未知错误");
72
+ const safeDetail = String(detail || "");
73
+
74
+ if (shouldSkipError(safeErrorType, safeMessage, safeDetail)) {
75
+ return false;
76
+ }
77
+
78
+ if (reporting) {
79
+ return false;
80
+ }
81
+
82
+ reporting = true;
83
+
84
+ try {
85
+ await request(ERROR_REPORT_PATH, buildReportData(options, safeErrorType, safeMessage, safeDetail, extraData));
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ } finally {
90
+ reporting = false;
91
+ }
92
+ }
93
+ };
94
+ }
95
+
96
+ export function setupErrorReport(options) {
97
+ const errorReporter = createErrorReport(options);
98
+
99
+ function reportClientError(errorType, message, detail) {
100
+ void errorReporter.reportError(errorType, message, detail);
101
+ }
102
+
103
+ if (options.reportHttp !== false) {
104
+ setupFetchErrorReport(options, reportClientError);
105
+ }
106
+
107
+ if (options.app?.config && options.reportVue !== false) {
108
+ options.app.config.errorHandler = (error, _instance, info) => {
109
+ const message = getErrorMessage(error);
110
+ const detail = `${String(info || "")}${info ? "\n" : ""}${getErrorDetail(error)}`;
111
+
112
+ reportClientError("vue", message, detail);
113
+ };
114
+ }
115
+
116
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
117
+ if (options.reportWindow !== false) {
118
+ window.addEventListener(
119
+ "error",
120
+ (event) => {
121
+ const target = event.target;
122
+
123
+ if (target && target !== window) {
124
+ if (options.reportResource === false) {
125
+ return;
126
+ }
127
+
128
+ const resourceName = target.tagName ? `${String(target.tagName).toLowerCase()} 资源加载失败` : "资源加载失败";
129
+ const parts = [];
130
+
131
+ if (target?.tagName) {
132
+ parts.push(`tag: ${String(target.tagName).toLowerCase()}`);
133
+ }
134
+
135
+ if (target?.src) {
136
+ parts.push(`src: ${target.src}`);
137
+ }
138
+
139
+ if (target?.href) {
140
+ parts.push(`href: ${target.href}`);
141
+ }
142
+
143
+ reportClientError("resource", resourceName, parts.join("\n"));
144
+ return;
145
+ }
146
+
147
+ const parts = [];
148
+
149
+ if (event.filename) {
150
+ parts.push(`file: ${event.filename}`);
151
+ }
152
+
153
+ if (event.lineno || event.colno) {
154
+ parts.push(`line: ${event.lineno || 0}, col: ${event.colno || 0}`);
155
+ }
156
+
157
+ const stack = getErrorDetail(event.error);
158
+
159
+ if (stack) {
160
+ parts.push(stack);
161
+ }
162
+
163
+ reportClientError("window", getErrorMessage(event.message || event.error), parts.join("\n"));
164
+ },
165
+ true
166
+ );
167
+ }
168
+
169
+ if (options.reportPromise !== false) {
170
+ window.addEventListener("unhandledrejection", (event) => {
171
+ const reason = event.reason;
172
+
173
+ reportClientError("promise", getErrorMessage(reason), getErrorDetail(reason));
174
+ });
175
+ }
176
+ }
177
+
178
+ if (options.router && typeof options.router.onError === "function" && options.reportRouter !== false) {
179
+ options.router.onError((error) => {
180
+ reportClientError("router", getErrorMessage(error), getErrorDetail(error));
181
+ });
182
+ }
183
+
184
+ return {
185
+ reporter: errorReporter,
186
+ reportClientError: reportClientError
187
+ };
188
+ }
189
+
190
+ function setupFetchErrorReport(options, reportClientError) {
191
+ if (fetchWrapped) {
192
+ return;
193
+ }
194
+
195
+ if (typeof window === "undefined" || typeof window.fetch !== "function") {
196
+ return;
197
+ }
198
+
199
+ const originalFetch = window.fetch.bind(window);
200
+
201
+ window.fetch = async (...args) => {
202
+ const input = args[0];
203
+ const url = typeof input === "string" ? input : input && typeof input.url === "string" ? input.url : "";
204
+ const apiPath = String(options.apiPath || "");
205
+ const isApiRequest = Boolean(apiPath) && String(url || "").startsWith(apiPath);
206
+ const isErrorReportRequest = String(url || "").includes("/core/tongJi/errorReport");
207
+
208
+ try {
209
+ const response = await originalFetch(...args);
210
+
211
+ if (isApiRequest && !isErrorReportRequest) {
212
+ if (!response.ok) {
213
+ const parts = [];
214
+
215
+ if (url) {
216
+ parts.push(`url: ${url}`);
217
+ }
218
+
219
+ if (response.status) {
220
+ parts.push(`status: ${response.status}`);
221
+ }
222
+
223
+ reportClientError("http", `请求失败:HTTP ${response.status}`, parts.join("\n"));
224
+ } else {
225
+ try {
226
+ const payload = await response.clone().json();
227
+
228
+ if (payload?.code !== 0) {
229
+ const parts = [];
230
+
231
+ if (url) {
232
+ parts.push(`url: ${url}`);
233
+ }
234
+
235
+ if (response.status) {
236
+ parts.push(`status: ${response.status}`);
237
+ }
238
+
239
+ if (payload?.code !== undefined) {
240
+ parts.push(`code: ${payload.code}`);
241
+ }
242
+
243
+ if (payload?.msg) {
244
+ parts.push(`msg: ${payload.msg}`);
245
+ }
246
+
247
+ if (payload?.detail) {
248
+ parts.push(`detail: ${payload.detail}`);
249
+ }
250
+
251
+ reportClientError("http", payload.msg || "请求失败", parts.join("\n"));
252
+ }
253
+ } catch {
254
+ // ignore
255
+ }
256
+ }
257
+ }
258
+
259
+ return response;
260
+ } catch (error) {
261
+ if (isApiRequest && !isErrorReportRequest) {
262
+ const parts = [];
263
+
264
+ if (url) {
265
+ parts.push(`url: ${url}`);
266
+ }
267
+
268
+ const errorDetail = getErrorDetail(error);
269
+
270
+ if (errorDetail) {
271
+ parts.push(`detail: ${errorDetail}`);
272
+ }
273
+
274
+ reportClientError("http", getErrorMessage(error), parts.join("\n"));
275
+ }
276
+
277
+ throw error;
278
+ }
279
+ };
280
+
281
+ fetchWrapped = true;
282
+ }
@@ -0,0 +1,31 @@
1
+ import { appendPageAndExtraData, createCommonReportData, getReportRequest, getRouteReportData } from "../internal/reportUtil.js";
2
+
3
+ const INFO_REPORT_PATH = "/core/tongJi/infoReport";
4
+
5
+ function buildReportData(options, extraData) {
6
+ return appendPageAndExtraData(createCommonReportData(options), options, extraData);
7
+ }
8
+
9
+ export function createInfoReport(options) {
10
+ const request = getReportRequest(options, "createInfoReport");
11
+
12
+ return {
13
+ reportInfo: function (extraData = {}) {
14
+ return request(INFO_REPORT_PATH, buildReportData(options, extraData));
15
+ }
16
+ };
17
+ }
18
+
19
+ export function setupInfoReport(options) {
20
+ const infoReporter = createInfoReport(options);
21
+
22
+ if (options.router && typeof options.router.afterEach === "function") {
23
+ options.router.afterEach((to) => {
24
+ void infoReporter.reportInfo(getRouteReportData(to)).catch(() => {
25
+ // ignore
26
+ });
27
+ });
28
+ }
29
+
30
+ return infoReporter;
31
+ }
@@ -0,0 +1,155 @@
1
+ import { appendPageAndExtraData, createCommonReportData, getReportRequest } from "../internal/reportUtil.js";
2
+
3
+ const ONLINE_REPORT_PATH = "/core/tongJi/onlineReport";
4
+ const ONLINE_REPORT_MIN_GAP = 15 * 1000;
5
+ const ONLINE_REPORT_HEARTBEAT_INTERVAL = 60 * 1000;
6
+ const ONLINE_REPORT_CLIENT_ID_KEY = "befly-online-client-id";
7
+
8
+ function generateOnlineClientId() {
9
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
10
+ }
11
+
12
+ function readStoredClientId(storageKey) {
13
+ if (typeof globalThis.localStorage === "undefined") {
14
+ return "";
15
+ }
16
+
17
+ try {
18
+ return String(globalThis.localStorage.getItem(storageKey) || "").trim();
19
+ } catch {
20
+ return "";
21
+ }
22
+ }
23
+
24
+ function writeStoredClientId(storageKey, clientId) {
25
+ if (typeof globalThis.localStorage === "undefined") {
26
+ return;
27
+ }
28
+
29
+ try {
30
+ globalThis.localStorage.setItem(storageKey, clientId);
31
+ } catch {
32
+ // ignore
33
+ }
34
+ }
35
+
36
+ function buildReportData(options, clientId, extraData) {
37
+ const data = createCommonReportData(options);
38
+ data.clientId = clientId;
39
+
40
+ return appendPageAndExtraData(data, options, extraData);
41
+ }
42
+
43
+ function isDocumentActive() {
44
+ if (typeof document !== "undefined" && document.visibilityState !== "visible") {
45
+ return false;
46
+ }
47
+
48
+ return true;
49
+ }
50
+
51
+ export function createOnlineReport(options) {
52
+ const request = getReportRequest(options, "createOnlineReport");
53
+
54
+ let heartbeatStarted = false;
55
+ let lastReportTime = 0;
56
+ let timeoutId = 0;
57
+
58
+ function getClientId() {
59
+ if (typeof options.getClientId === "function") {
60
+ return String(options.getClientId() || "");
61
+ }
62
+
63
+ const storedClientId = readStoredClientId(ONLINE_REPORT_CLIENT_ID_KEY);
64
+ if (storedClientId) {
65
+ return storedClientId;
66
+ }
67
+
68
+ const clientId = generateOnlineClientId();
69
+ writeStoredClientId(ONLINE_REPORT_CLIENT_ID_KEY, clientId);
70
+ return clientId;
71
+ }
72
+
73
+ function clearHeartbeatTimer() {
74
+ if (!timeoutId || typeof globalThis.clearTimeout !== "function") {
75
+ timeoutId = 0;
76
+ return;
77
+ }
78
+
79
+ globalThis.clearTimeout(timeoutId);
80
+ timeoutId = 0;
81
+ }
82
+
83
+ function queueHeartbeat() {
84
+ if (typeof globalThis.setTimeout !== "function") {
85
+ return;
86
+ }
87
+
88
+ clearHeartbeatTimer();
89
+
90
+ timeoutId = globalThis.setTimeout(() => {
91
+ void triggerOnlineReport();
92
+ queueHeartbeat();
93
+ }, ONLINE_REPORT_HEARTBEAT_INTERVAL);
94
+ }
95
+
96
+ async function reportOnline(extraData = {}) {
97
+ lastReportTime = Date.now();
98
+ return request(ONLINE_REPORT_PATH, buildReportData(options, getClientId(), extraData));
99
+ }
100
+
101
+ async function triggerOnlineReport(force = false, extraData = {}) {
102
+ if (!isDocumentActive()) {
103
+ return false;
104
+ }
105
+
106
+ const now = Date.now();
107
+
108
+ if (!force && now - lastReportTime < ONLINE_REPORT_MIN_GAP) {
109
+ return false;
110
+ }
111
+
112
+ try {
113
+ await reportOnline(extraData);
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ function startHeartbeat() {
121
+ if (heartbeatStarted) {
122
+ return;
123
+ }
124
+
125
+ heartbeatStarted = true;
126
+
127
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
128
+ window.addEventListener("focus", () => {
129
+ void triggerOnlineReport();
130
+ });
131
+ }
132
+
133
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
134
+ document.addEventListener("visibilitychange", () => {
135
+ void triggerOnlineReport();
136
+ });
137
+ }
138
+
139
+ void triggerOnlineReport(true);
140
+ queueHeartbeat();
141
+ }
142
+
143
+ return {
144
+ reportOnline: reportOnline,
145
+ startHeartbeat: startHeartbeat
146
+ };
147
+ }
148
+
149
+ export function setupOnlineReport(options) {
150
+ const onlineReporter = createOnlineReport(options);
151
+
152
+ onlineReporter.startHeartbeat();
153
+
154
+ return onlineReporter;
155
+ }