@fz-firefly/web 0.0.0-stage → 0.2.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/dist/index.js ADDED
@@ -0,0 +1,876 @@
1
+ import { FireflyTransportError, captureHttpFailure, captureHttpRequest, createClient, installClickBreadcrumbs, installConsoleBreadcrumbs, installLifecycleBreadcrumbs, installResourceErrorHooks, isIngestRequestUrl, parseRetryAfter, resolveOptions, serializeHttpBody, serializeHttpResponseBody } from "@fz-firefly/core";
2
+ // src/hooks/global-errors.ts
3
+ /**
4
+ * 安装全局错误钩子。
5
+ *
6
+ * 覆盖两类未捕获错误:
7
+ * 1. window 'error':运行时异常(资源加载失败由 resource-errors 单独处理);
8
+ * 2. window 'unhandledrejection':未处理的 Promise 拒绝。
9
+ *
10
+ * 全部使用 addEventListener,不覆盖 window.onerror,
11
+ * 业务自己挂的处理器仍然有效。
12
+ *
13
+ * @returns 卸载函数
14
+ */
15
+ function installGlobalErrorHooks(client) {
16
+ const handleError = (event) => {
17
+ try {
18
+ const target = event.target;
19
+ if (target && typeof target.tagName === "string") return;
20
+ if (event.error) client.captureException(event.error);
21
+ else if (event.message) client.captureException(new Error(event.message));
22
+ } catch (_unused) {}
23
+ };
24
+ const handleRejection = (event) => {
25
+ try {
26
+ const reason = event.reason;
27
+ client.captureException(reason !== null && reason !== void 0 ? reason : /* @__PURE__ */ new Error("Unhandled promise rejection"));
28
+ } catch (_unused2) {}
29
+ };
30
+ window.addEventListener("error", handleError);
31
+ window.addEventListener("unhandledrejection", handleRejection);
32
+ return () => {
33
+ window.removeEventListener("error", handleError);
34
+ window.removeEventListener("unhandledrejection", handleRejection);
35
+ };
36
+ }
37
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/typeof.js
38
+ function _typeof(o) {
39
+ "@babel/helpers - typeof";
40
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
41
+ return typeof o;
42
+ } : function(o) {
43
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
44
+ }, _typeof(o);
45
+ }
46
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPrimitive.js
47
+ function toPrimitive(t, r) {
48
+ if ("object" != _typeof(t) || !t) return t;
49
+ var e = t[Symbol.toPrimitive];
50
+ if (void 0 !== e) {
51
+ var i = e.call(t, r || "default");
52
+ if ("object" != _typeof(i)) return i;
53
+ throw new TypeError("@@toPrimitive must return a primitive value.");
54
+ }
55
+ return ("string" === r ? String : Number)(t);
56
+ }
57
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPropertyKey.js
58
+ function toPropertyKey(t) {
59
+ var i = toPrimitive(t, "string");
60
+ return "symbol" == _typeof(i) ? i : i + "";
61
+ }
62
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/defineProperty.js
63
+ function _defineProperty(e, r, t) {
64
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
65
+ value: t,
66
+ enumerable: !0,
67
+ configurable: !0,
68
+ writable: !0
69
+ }) : e[r] = t, e;
70
+ }
71
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/objectSpread2.js
72
+ function ownKeys(e, r) {
73
+ var t = Object.keys(e);
74
+ if (Object.getOwnPropertySymbols) {
75
+ var o = Object.getOwnPropertySymbols(e);
76
+ r && (o = o.filter(function(r) {
77
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
78
+ })), t.push.apply(t, o);
79
+ }
80
+ return t;
81
+ }
82
+ function _objectSpread2(e) {
83
+ for (var r = 1; r < arguments.length; r++) {
84
+ var t = null != arguments[r] ? arguments[r] : {};
85
+ r % 2 ? ownKeys(Object(t), !0).forEach(function(r) {
86
+ _defineProperty(e, r, t[r]);
87
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r) {
88
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
89
+ });
90
+ }
91
+ return e;
92
+ }
93
+ // src/hooks/http-errors.ts
94
+ /** 响应头白名单,避免把 Set-Cookie 等敏感头带进监控。 */
95
+ const RESPONSE_HEADER_WHITELIST = [
96
+ "content-type",
97
+ "content-length",
98
+ "x-request-id",
99
+ "x-trace-id",
100
+ "traceparent",
101
+ "x-b3-traceid"
102
+ ];
103
+ /** 读取请求头里的 content-type,兼容 Headers / 数组 / 普通对象。 */
104
+ function headerValue(headers, name) {
105
+ if (!headers) return "";
106
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
107
+ var _headers$get;
108
+ return (_headers$get = headers.get(name)) !== null && _headers$get !== void 0 ? _headers$get : "";
109
+ }
110
+ if (Array.isArray(headers)) {
111
+ var _pair$;
112
+ const pair = headers.find(([key]) => key.toLowerCase() === name);
113
+ return (_pair$ = pair === null || pair === void 0 ? void 0 : pair[1]) !== null && _pair$ !== void 0 ? _pair$ : "";
114
+ }
115
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === name) return String(value);
116
+ return "";
117
+ }
118
+ /** 只保留白名单内的响应头。 */
119
+ function pickResponseHeaders(headers) {
120
+ if (!headers) return void 0;
121
+ const result = {};
122
+ for (const name of RESPONSE_HEADER_WHITELIST) {
123
+ const value = headers.get(name);
124
+ if (value) result[name] = value;
125
+ }
126
+ return Object.keys(result).length > 0 ? result : void 0;
127
+ }
128
+ /** 从响应头里取 request id / trace id。 */
129
+ function requestIdOf(headers) {
130
+ if (!headers) return void 0;
131
+ for (const name of [
132
+ "x-request-id",
133
+ "x-trace-id",
134
+ "traceparent",
135
+ "x-b3-traceid"
136
+ ]) {
137
+ const value = headers.get(name);
138
+ if (value) return value;
139
+ }
140
+ }
141
+ /** 解析 content-length;非法值返回 undefined。 */
142
+ function contentLengthOf(headers) {
143
+ const value = headers === null || headers === void 0 ? void 0 : headers.get("content-length");
144
+ if (!value) return void 0;
145
+ const parsed = Number(value);
146
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
147
+ }
148
+ /** 估算响应体大小,content-length 缺失时退化为字符数。 */
149
+ function sizeOf(body) {
150
+ if (!body) return void 0;
151
+ if (typeof body.value === "string") return body.value.length;
152
+ try {
153
+ return JSON.stringify(body.value).length;
154
+ } catch (_unused) {
155
+ return;
156
+ }
157
+ }
158
+ /** 判断 fetch 抛出的错误是否只是主动取消。 */
159
+ function isAbortError(error) {
160
+ return typeof error === "object" && error !== null && error.name === "AbortError";
161
+ }
162
+ /**
163
+ * 准备一个按需读取请求体的函数。
164
+ *
165
+ * Request 的 clone 必须在 fetch 之前完成,但真正读取要等确认请求失败
166
+ * 或确认是慢请求,避免成功请求也去读一遍 body。
167
+ */
168
+ function prepareFetchBody(input, init, contentType, options) {
169
+ if ((init === null || init === void 0 ? void 0 : init.body) !== void 0 && init.body !== null) {
170
+ const body = init.body;
171
+ return async () => serializeHttpBody(body, contentType, options);
172
+ }
173
+ if (typeof Request !== "undefined" && input instanceof Request && input.body) try {
174
+ const clone = input.clone();
175
+ return async () => {
176
+ try {
177
+ const text = await clone.text();
178
+ return serializeHttpBody(text, contentType || "application/json", options);
179
+ } catch (_unused2) {
180
+ return;
181
+ }
182
+ };
183
+ } catch (_unused3) {}
184
+ return async () => void 0;
185
+ }
186
+ /** 单次响应体采集最多读取的字节数;避免为大文件把整个 body 读进内存。 */
187
+ const MAX_RESPONSE_READ_BYTES = 524288;
188
+ /** 响应体截断标记。 */
189
+ const TRUNCATED_MARKER = "\n…[truncated]…\n";
190
+ /** 与 shared 的 isTextContentType 对齐;用于在读取前跳过二进制。 */
191
+ function isTextResponseContentType(contentType) {
192
+ const value = contentType.toLowerCase();
193
+ return value === "" || value.startsWith("text/") || value.includes("json") || value.includes("xml") || value.includes("javascript") || value.includes("x-www-form-urlencoded");
194
+ }
195
+ function isJsonResponseContentType(contentType) {
196
+ const value = contentType.toLowerCase();
197
+ return value.includes("application/json") || value.includes("+json");
198
+ }
199
+ function concatBytes(chunks, total) {
200
+ const bytes = new Uint8Array(total);
201
+ let offset = 0;
202
+ for (const chunk of chunks) {
203
+ bytes.set(chunk, offset);
204
+ offset += chunk.length;
205
+ }
206
+ return bytes;
207
+ }
208
+ function decodeBytes(bytes) {
209
+ if (typeof TextDecoder !== "undefined") return new TextDecoder().decode(bytes);
210
+ let text = "";
211
+ for (let index = 0; index < bytes.length; index += 1) text += String.fromCharCode(bytes[index]);
212
+ return text;
213
+ }
214
+ /** 把文本压到 maxChars 以内,并尽量保留头部和尾部。 */
215
+ function compactText(text, maxChars) {
216
+ if (text.length <= maxChars) return {
217
+ text,
218
+ truncated: false
219
+ };
220
+ const tailChars = Math.min(1024, Math.max(0, Math.floor(maxChars / 4)));
221
+ const headChars = Math.max(0, maxChars - tailChars - 15);
222
+ return {
223
+ text: `${text.slice(0, headChars)}${TRUNCATED_MARKER}${text.slice(-tailChars)}`,
224
+ truncated: true
225
+ };
226
+ }
227
+ /**
228
+ * 从 clone 的 ReadableStream 限量读取响应体。
229
+ *
230
+ * 最多读 512KB,避免大文件把内存打满;文本超过 maxChars 时保留头尾,
231
+ * 尾部通常包含错误码/错误提示,比单纯截断前 N 字符更有用。
232
+ */
233
+ async function readLimitedResponseBody(clone, contentType, options) {
234
+ if (!isTextResponseContentType(contentType)) return void 0;
235
+ if (!clone.body) return void 0;
236
+ const reader = clone.body.getReader();
237
+ const chunks = [];
238
+ let total = 0;
239
+ let hitLimit = false;
240
+ try {
241
+ while (total < MAX_RESPONSE_READ_BYTES) {
242
+ const { done, value } = await reader.read();
243
+ if (done) break;
244
+ if (!value || value.length === 0) continue;
245
+ const remaining = MAX_RESPONSE_READ_BYTES - total;
246
+ if (value.length > remaining) {
247
+ chunks.push(value.slice(0, remaining));
248
+ total += remaining;
249
+ hitLimit = true;
250
+ break;
251
+ }
252
+ chunks.push(value);
253
+ total += value.length;
254
+ }
255
+ } catch (_unused4) {
256
+ return;
257
+ } finally {
258
+ try {
259
+ await reader.cancel();
260
+ } catch (_unused5) {}
261
+ }
262
+ const text = decodeBytes(concatBytes(chunks, total));
263
+ const maxChars = options.maxChars && options.maxChars > 0 ? options.maxChars : 4096;
264
+ if (text.length <= maxChars) return serializeHttpResponseBody(text, contentType, _objectSpread2(_objectSpread2({}, options), {}, { maxChars }));
265
+ if (hitLimit && isJsonResponseContentType(contentType)) return;
266
+ if (isJsonResponseContentType(contentType)) {
267
+ const parsed = serializeHttpBody(text, contentType, _objectSpread2(_objectSpread2({}, options), {}, { maxChars: text.length + 1 }));
268
+ if (parsed) try {
269
+ const json = JSON.stringify(parsed.value);
270
+ if (json) return {
271
+ value: compactText(json, maxChars).text,
272
+ truncated: true
273
+ };
274
+ } catch (_unused6) {}
275
+ }
276
+ const compacted = compactText(text, maxChars);
277
+ const capture = serializeHttpResponseBody(compacted.text, contentType, _objectSpread2(_objectSpread2({}, options), {}, { maxChars }));
278
+ if (!capture) return void 0;
279
+ return {
280
+ value: capture.value,
281
+ truncated: capture.truncated || compacted.truncated || hitLimit
282
+ };
283
+ }
284
+ /** 自动记录请求日志、失败请求事件和慢请求面包屑。 */
285
+ function installHttpErrorHooks(client, options) {
286
+ var _options$slowRequestT;
287
+ if (options.captureHttp === false) return () => {};
288
+ const captureBody = options.captureHttpBody !== false;
289
+ const captureResponseBody = options.captureHttpResponseBody !== false;
290
+ const requestBodyOptions = {
291
+ redactKeys: options.redactKeys,
292
+ maxChars: options.maxHttpBodyChars
293
+ };
294
+ const responseBodyOptions = {
295
+ redactKeys: options.redactKeys,
296
+ maxChars: options.maxHttpResponseChars
297
+ };
298
+ const slowThresholdMs = (_options$slowRequestT = options.slowRequestThresholdMs) !== null && _options$slowRequestT !== void 0 ? _options$slowRequestT : 1e3;
299
+ const restores = [];
300
+ installFetchHook(client, captureBody, captureResponseBody, requestBodyOptions, responseBodyOptions, slowThresholdMs, restores);
301
+ installXhrHook(client, captureBody, captureResponseBody, requestBodyOptions, responseBodyOptions, slowThresholdMs, restores);
302
+ return () => {
303
+ for (const restore of restores) restore();
304
+ };
305
+ }
306
+ function installFetchHook(client, captureBody, captureResponseBody, requestBodyOptions, responseBodyOptions, slowThresholdMs, restores) {
307
+ if (typeof window === "undefined" || typeof window.fetch !== "function") return;
308
+ const originalFetch = window.fetch;
309
+ const wrapped = async function(input, init) {
310
+ var _response$headers$get;
311
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
312
+ let method;
313
+ let url;
314
+ try {
315
+ var _ref, _init$method, _request$url;
316
+ method = ((_ref = (_init$method = init === null || init === void 0 ? void 0 : init.method) !== null && _init$method !== void 0 ? _init$method : request === null || request === void 0 ? void 0 : request.method) !== null && _ref !== void 0 ? _ref : "GET").toUpperCase();
317
+ url = (_request$url = request === null || request === void 0 ? void 0 : request.url) !== null && _request$url !== void 0 ? _request$url : String(input);
318
+ } catch (_unused7) {
319
+ return originalFetch.call(this, input, init);
320
+ }
321
+ if (isIngestRequestUrl(url)) return originalFetch.call(this, input, init);
322
+ let readBody = async () => void 0;
323
+ try {
324
+ const contentType = headerValue(init === null || init === void 0 ? void 0 : init.headers, "content-type") || (request === null || request === void 0 ? void 0 : request.headers.get("content-type")) || "";
325
+ readBody = captureBody ? prepareFetchBody(input, init, contentType, requestBodyOptions) : async () => void 0;
326
+ } catch (_unused8) {}
327
+ const startedAt = Date.now();
328
+ let response;
329
+ try {
330
+ response = await originalFetch.call(this, input, init);
331
+ } catch (error) {
332
+ if (!isAbortError(error)) (async () => {
333
+ try {
334
+ const requestBody = await readBody();
335
+ captureHttpFailure(client, {
336
+ method,
337
+ url,
338
+ statusCode: 0,
339
+ durationMs: Date.now() - startedAt,
340
+ category: "fetch",
341
+ startedAt,
342
+ requestBody: requestBody === null || requestBody === void 0 ? void 0 : requestBody.value,
343
+ requestBodyTruncated: requestBody === null || requestBody === void 0 ? void 0 : requestBody.truncated,
344
+ error: error instanceof Error ? error.message : String(error)
345
+ });
346
+ } catch (_unused9) {}
347
+ })();
348
+ throw error;
349
+ }
350
+ const durationMs = Date.now() - startedAt;
351
+ const ok = response.status >= 200 && response.status < 400;
352
+ const slow = durationMs >= slowThresholdMs;
353
+ const responseContentType = (_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 ? _response$headers$get : "";
354
+ const responseHeaders = pickResponseHeaders(response.headers);
355
+ const requestId = requestIdOf(response.headers);
356
+ const responseSize = contentLengthOf(response.headers);
357
+ const capture = (requestBody, responseBody) => {
358
+ try {
359
+ captureHttpRequest(client, {
360
+ method,
361
+ url,
362
+ statusCode: response.status,
363
+ durationMs,
364
+ category: "fetch",
365
+ ok,
366
+ startedAt,
367
+ requestBody: requestBody === null || requestBody === void 0 ? void 0 : requestBody.value,
368
+ requestBodyTruncated: requestBody === null || requestBody === void 0 ? void 0 : requestBody.truncated,
369
+ responseBody: responseBody === null || responseBody === void 0 ? void 0 : responseBody.value,
370
+ responseBodyTruncated: responseBody === null || responseBody === void 0 ? void 0 : responseBody.truncated,
371
+ responseHeaders,
372
+ requestId,
373
+ responseSize: responseSize !== null && responseSize !== void 0 ? responseSize : sizeOf(responseBody),
374
+ slowThresholdMs
375
+ });
376
+ } catch (_unused10) {}
377
+ };
378
+ if (!(!ok || slow) || !captureBody && !captureResponseBody) {
379
+ capture();
380
+ return response;
381
+ }
382
+ let responseClone;
383
+ if (captureResponseBody) try {
384
+ responseClone = response.clone();
385
+ } catch (_unused11) {}
386
+ (async () => {
387
+ let requestBody;
388
+ if (captureBody) try {
389
+ requestBody = await readBody();
390
+ } catch (_unused12) {}
391
+ try {
392
+ const responseBody = responseClone ? await readLimitedResponseBody(responseClone, responseContentType, responseBodyOptions) : void 0;
393
+ capture(requestBody, responseBody);
394
+ } catch (_unused13) {
395
+ capture(requestBody, void 0);
396
+ }
397
+ })();
398
+ return response;
399
+ };
400
+ try {
401
+ window.fetch = wrapped;
402
+ restores.push(() => {
403
+ if (window.fetch === wrapped) window.fetch = originalFetch;
404
+ });
405
+ } catch (_unused14) {}
406
+ }
407
+ function installXhrHook(client, captureBody, captureResponseBody, requestBodyOptions, responseBodyOptions, slowThresholdMs, restores) {
408
+ if (typeof window === "undefined" || typeof window.XMLHttpRequest !== "function") return;
409
+ const XHR = window.XMLHttpRequest;
410
+ const meta = /* @__PURE__ */ new WeakMap();
411
+ const originalOpen = XHR.prototype.open;
412
+ const originalSend = XHR.prototype.send;
413
+ const originalSetRequestHeader = XHR.prototype.setRequestHeader;
414
+ const originalAbort = XHR.prototype.abort;
415
+ const wrappedOpen = function(method, url, async, username, password) {
416
+ if (isIngestRequestUrl(String(url))) {
417
+ meta.delete(this);
418
+ return originalOpen.call(this, method, url, async !== null && async !== void 0 ? async : true, username, password);
419
+ }
420
+ meta.set(this, {
421
+ method: String(method).toUpperCase(),
422
+ url: String(url),
423
+ contentType: "",
424
+ body: void 0,
425
+ startedAt: 0,
426
+ aborted: false
427
+ });
428
+ return originalOpen.call(this, method, url, async !== null && async !== void 0 ? async : true, username, password);
429
+ };
430
+ const wrappedSend = function(body) {
431
+ const info = meta.get(this);
432
+ if (!info) {
433
+ originalSend.call(this, body);
434
+ return;
435
+ }
436
+ info.body = body;
437
+ info.startedAt = Date.now();
438
+ const onLoadEnd = function() {
439
+ const current = meta.get(this);
440
+ if (!current || current.aborted) return;
441
+ try {
442
+ var _ref2, _ref3, _responseHeaders$xRe, _responseHeaders$cont, _contentLengthOfValue;
443
+ const status = this.status;
444
+ const ok = status >= 200 && status < 400;
445
+ const durationMs = Date.now() - current.startedAt;
446
+ const slow = durationMs >= slowThresholdMs;
447
+ const responseHeaders = parseXhrResponseHeaders(this);
448
+ const requestId = (_ref2 = (_ref3 = (_responseHeaders$xRe = responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders["x-request-id"]) !== null && _responseHeaders$xRe !== void 0 ? _responseHeaders$xRe : responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders["x-trace-id"]) !== null && _ref3 !== void 0 ? _ref3 : responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders["traceparent"]) !== null && _ref2 !== void 0 ? _ref2 : responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders["x-b3-traceid"];
449
+ const contentType = (_responseHeaders$cont = responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders["content-type"]) !== null && _responseHeaders$cont !== void 0 ? _responseHeaders$cont : "";
450
+ const requestBodyCapture = (!ok || slow) && captureBody ? serializeHttpBody(current.body, current.contentType, requestBodyOptions) : void 0;
451
+ const responseBodyCapture = (!ok || slow) && captureResponseBody ? serializeXhrResponseBody(this, contentType, responseBodyOptions) : void 0;
452
+ captureHttpRequest(client, _objectSpread2({
453
+ method: current.method,
454
+ url: current.url,
455
+ statusCode: status,
456
+ durationMs,
457
+ category: "xhr",
458
+ ok,
459
+ startedAt: current.startedAt,
460
+ requestBody: requestBodyCapture === null || requestBodyCapture === void 0 ? void 0 : requestBodyCapture.value,
461
+ requestBodyTruncated: requestBodyCapture === null || requestBodyCapture === void 0 ? void 0 : requestBodyCapture.truncated,
462
+ responseBody: responseBodyCapture === null || responseBodyCapture === void 0 ? void 0 : responseBodyCapture.value,
463
+ responseBodyTruncated: responseBodyCapture === null || responseBodyCapture === void 0 ? void 0 : responseBodyCapture.truncated,
464
+ responseHeaders,
465
+ requestId,
466
+ responseSize: (_contentLengthOfValue = contentLengthOfValue(responseHeaders === null || responseHeaders === void 0 ? void 0 : responseHeaders["content-length"])) !== null && _contentLengthOfValue !== void 0 ? _contentLengthOfValue : sizeOf(responseBodyCapture),
467
+ slowThresholdMs
468
+ }, status === 0 ? { error: "network error or timeout" } : {}));
469
+ } catch (_unused15) {}
470
+ };
471
+ this.addEventListener("loadend", onLoadEnd, { once: true });
472
+ originalSend.call(this, body);
473
+ };
474
+ const wrappedSetRequestHeader = function(name, value) {
475
+ const info = meta.get(this);
476
+ if (info && name.toLowerCase() === "content-type") info.contentType = value;
477
+ return originalSetRequestHeader.call(this, name, value);
478
+ };
479
+ const wrappedAbort = function() {
480
+ const info = meta.get(this);
481
+ if (info) info.aborted = true;
482
+ return originalAbort.call(this);
483
+ };
484
+ try {
485
+ XHR.prototype.open = wrappedOpen;
486
+ XHR.prototype.send = wrappedSend;
487
+ XHR.prototype.setRequestHeader = wrappedSetRequestHeader;
488
+ XHR.prototype.abort = wrappedAbort;
489
+ restores.push(() => {
490
+ if (XHR.prototype.open === wrappedOpen) XHR.prototype.open = originalOpen;
491
+ if (XHR.prototype.send === wrappedSend) XHR.prototype.send = originalSend;
492
+ if (XHR.prototype.setRequestHeader === wrappedSetRequestHeader) XHR.prototype.setRequestHeader = originalSetRequestHeader;
493
+ if (XHR.prototype.abort === wrappedAbort) XHR.prototype.abort = originalAbort;
494
+ });
495
+ } catch (_unused16) {}
496
+ }
497
+ /** 解析 XHR 的响应头;部分环境不实现 getAllResponseHeaders。 */
498
+ function parseXhrResponseHeaders(xhr) {
499
+ try {
500
+ var _xhr$getAllResponseHe;
501
+ const raw = (_xhr$getAllResponseHe = xhr.getAllResponseHeaders) === null || _xhr$getAllResponseHe === void 0 ? void 0 : _xhr$getAllResponseHe.call(xhr);
502
+ if (!raw) return void 0;
503
+ const result = {};
504
+ for (const line of raw.trim().split(/[\r\n]+/)) {
505
+ const index = line.indexOf(":");
506
+ if (index <= 0) continue;
507
+ const key = line.slice(0, index).trim().toLowerCase();
508
+ const value = line.slice(index + 1).trim();
509
+ if (RESPONSE_HEADER_WHITELIST.includes(key) && value) result[key] = value;
510
+ }
511
+ return Object.keys(result).length > 0 ? result : void 0;
512
+ } catch (_unused17) {
513
+ return;
514
+ }
515
+ }
516
+ /** 读取 XHR 响应体;二进制响应不读取。 */
517
+ function serializeXhrResponseBody(xhr, contentType, options) {
518
+ try {
519
+ const responseType = xhr.responseType;
520
+ if (responseType && responseType !== "text" && responseType !== "json") return;
521
+ const body = responseType === "json" ? xhr.response : xhr.responseText;
522
+ if (typeof body === "string") {
523
+ const maxChars = options.maxChars && options.maxChars > 0 ? options.maxChars : 4096;
524
+ const compacted = compactText(body, maxChars);
525
+ const capture = serializeHttpResponseBody(compacted.text, contentType, _objectSpread2(_objectSpread2({}, options), {}, { maxChars }));
526
+ if (!capture) return void 0;
527
+ return {
528
+ value: capture.value,
529
+ truncated: capture.truncated || compacted.truncated
530
+ };
531
+ }
532
+ return serializeHttpResponseBody(body, contentType, options);
533
+ } catch (_unused18) {
534
+ return;
535
+ }
536
+ }
537
+ /** 解析 content-length 字符串。 */
538
+ function contentLengthOfValue(value) {
539
+ if (!value) return void 0;
540
+ const parsed = Number(value);
541
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
542
+ }
543
+ // src/platform/context.ts
544
+ /**
545
+ * 创建浏览器上下文采集器。
546
+ *
547
+ * 只采集「定位问题需要、且不涉及隐私」的信息:
548
+ * 浏览器/系统/屏幕/网络/语言。
549
+ * 不采集输入框内容、Cookie、Authorization 头。
550
+ */
551
+ function createContext() {
552
+ return {
553
+ platform: "web",
554
+ pageUrl: () => {
555
+ var _window$location;
556
+ return typeof window === "undefined" ? void 0 : (_window$location = window.location) === null || _window$location === void 0 ? void 0 : _window$location.href;
557
+ },
558
+ pageRoute: () => {
559
+ var _window$location2;
560
+ return typeof window === "undefined" ? void 0 : (_window$location2 = window.location) === null || _window$location2 === void 0 ? void 0 : _window$location2.pathname;
561
+ },
562
+ collect() {
563
+ var _connection$effective, _window$screen, _window$screen2, _window$devicePixelRa;
564
+ const nav = window.navigator;
565
+ const ua = nav.userAgent;
566
+ const browser = detectBrowser(ua);
567
+ const connection = nav.connection;
568
+ return {
569
+ browser: {
570
+ name: browser,
571
+ version: detectBrowserVersion(ua, browser),
572
+ language: nav.language,
573
+ user_agent: ua,
574
+ viewport: {
575
+ width: window.innerWidth,
576
+ height: window.innerHeight
577
+ }
578
+ },
579
+ os: {
580
+ name: detectOs(ua),
581
+ version: detectOsVersion(ua)
582
+ },
583
+ device: { model: detectDevice(ua) },
584
+ network: {
585
+ effective_type: (_connection$effective = connection === null || connection === void 0 ? void 0 : connection.effectiveType) !== null && _connection$effective !== void 0 ? _connection$effective : "unknown",
586
+ online: nav.onLine !== false
587
+ },
588
+ screen: {
589
+ width: (_window$screen = window.screen) === null || _window$screen === void 0 ? void 0 : _window$screen.width,
590
+ height: (_window$screen2 = window.screen) === null || _window$screen2 === void 0 ? void 0 : _window$screen2.height,
591
+ pixel_ratio: (_window$devicePixelRa = window.devicePixelRatio) !== null && _window$devicePixelRa !== void 0 ? _window$devicePixelRa : 1
592
+ },
593
+ runtime: {
594
+ name: "web",
595
+ sdk: "@fz-firefly/web"
596
+ },
597
+ locale: {
598
+ language: nav.language,
599
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0
600
+ }
601
+ };
602
+ }
603
+ };
604
+ }
605
+ /** 从 UA 粗略识别浏览器。微信内置浏览器优先判断,否则会被识别成 Chrome。 */
606
+ function detectBrowser(ua) {
607
+ if (/MicroMessenger/i.test(ua)) return "WeChat";
608
+ if (/Edg\//.test(ua)) return "Edge";
609
+ if (/Chrome\//.test(ua)) return "Chrome";
610
+ if (/Firefox\//.test(ua)) return "Firefox";
611
+ if (/Safari\//.test(ua)) return "Safari";
612
+ return "Unknown";
613
+ }
614
+ /** 从 UA 提取浏览器版本。 */
615
+ function detectBrowserVersion(ua, browser) {
616
+ var _pattern$exec;
617
+ const pattern = {
618
+ WeChat: /MicroMessenger\/([\d.]+)/i,
619
+ Edge: /Edg\/([\d.]+)/i,
620
+ Chrome: /Chrome\/([\d.]+)/i,
621
+ Firefox: /Firefox\/([\d.]+)/i,
622
+ Safari: /Version\/([\d.]+).*Safari/i
623
+ }[browser];
624
+ return pattern === null || pattern === void 0 || (_pattern$exec = pattern.exec(ua)) === null || _pattern$exec === void 0 ? void 0 : _pattern$exec[1];
625
+ }
626
+ /** 从 UA 粗略识别操作系统。 */
627
+ function detectOs(ua) {
628
+ if (/Android/i.test(ua)) return "Android";
629
+ if (/iPhone|iPad|iPod/i.test(ua)) return "iOS";
630
+ if (/Windows/i.test(ua)) return "Windows";
631
+ if (/Mac OS X/i.test(ua)) return "macOS";
632
+ if (/Linux/i.test(ua)) return "Linux";
633
+ return "Unknown";
634
+ }
635
+ /** 从 UA 提取操作系统版本。 */
636
+ function detectOsVersion(ua) {
637
+ const candidates = [
638
+ [/Android\s([\d.]+)/i, (match) => {
639
+ var _match$;
640
+ return (_match$ = match[1]) !== null && _match$ !== void 0 ? _match$ : "";
641
+ }],
642
+ [/(?:iPhone|iPad|iPod).*?OS\s([\d_]+)/i, (match) => {
643
+ var _match$2;
644
+ return ((_match$2 = match[1]) !== null && _match$2 !== void 0 ? _match$2 : "").replace(/_/g, ".");
645
+ }],
646
+ [/Windows NT\s([\d.]+)/i, (match) => {
647
+ var _match$3;
648
+ return (_match$3 = match[1]) !== null && _match$3 !== void 0 ? _match$3 : "";
649
+ }],
650
+ [/Mac OS X\s([\d_]+)/i, (match) => {
651
+ var _match$4;
652
+ return ((_match$4 = match[1]) !== null && _match$4 !== void 0 ? _match$4 : "").replace(/_/g, ".");
653
+ }]
654
+ ];
655
+ for (const [pattern, format] of candidates) {
656
+ const match = pattern.exec(ua);
657
+ if (match) {
658
+ const value = format(match);
659
+ if (value) return value;
660
+ }
661
+ }
662
+ }
663
+ /** 设备型号。浏览器通常拿不到精确型号,小程序端会由适配层补上。 */
664
+ function detectDevice(ua) {
665
+ if (/iPhone/i.test(ua)) return "iPhone";
666
+ if (/iPad/i.test(ua)) return "iPad";
667
+ if (/Android/i.test(ua)) return "Android";
668
+ return "Desktop";
669
+ }
670
+ // src/platform/storage.ts
671
+ /**
672
+ * 创建浏览器存储。
673
+ *
674
+ * 优先用 localStorage 保证刷新/崩溃后还能补报;
675
+ * 隐私模式、配额满、被安全策略禁用时,localStorage 的读写会抛错,
676
+ * 这时退化为内存 Map —— 功能降级,但绝不能影响业务页面。
677
+ */
678
+ function createStorage() {
679
+ const memory = /* @__PURE__ */ new Map();
680
+ let useLocal = canUseLocalStorage();
681
+ return {
682
+ get(key) {
683
+ var _memory$get;
684
+ if (useLocal) try {
685
+ return window.localStorage.getItem(key);
686
+ } catch (_unused) {
687
+ useLocal = false;
688
+ }
689
+ return (_memory$get = memory.get(key)) !== null && _memory$get !== void 0 ? _memory$get : null;
690
+ },
691
+ set(key, value) {
692
+ memory.set(key, value);
693
+ if (useLocal) try {
694
+ window.localStorage.setItem(key, value);
695
+ } catch (_unused2) {
696
+ useLocal = false;
697
+ }
698
+ },
699
+ remove(key) {
700
+ memory.delete(key);
701
+ if (useLocal) try {
702
+ window.localStorage.removeItem(key);
703
+ } catch (_unused3) {
704
+ useLocal = false;
705
+ }
706
+ }
707
+ };
708
+ }
709
+ /** 探测 localStorage 是否可写。只执行一次,避免每次读写都付异常开销。 */
710
+ function canUseLocalStorage() {
711
+ try {
712
+ const probe = "__firefly_probe__";
713
+ window.localStorage.setItem(probe, "1");
714
+ window.localStorage.removeItem(probe);
715
+ return true;
716
+ } catch (_unused4) {
717
+ return false;
718
+ }
719
+ }
720
+ // src/platform/transport.ts
721
+ /**
722
+ * 创建浏览器上报通道。
723
+ *
724
+ * 使用 POST + text/plain + 查询参数传 key:
725
+ * text/plain 属于 CORS 简单请求,不会触发 OPTIONS 预检,
726
+ * 少了这一趟往返,页面崩溃边缘的上报成功率会高不少。
727
+ */
728
+ function createTransport(endpoint) {
729
+ const baseUrl = `${endpoint.replace(/\/+$/, "")}/api/ingest/v1/envelope`;
730
+ return {
731
+ async send(body, dsn, options) {
732
+ const url = `${baseUrl}?key=${encodeURIComponent(dsn)}`;
733
+ try {
734
+ if (typeof fetch === "function") {
735
+ const init = {
736
+ method: "POST",
737
+ headers: { "Content-Type": "text/plain;charset=UTF-8" },
738
+ body,
739
+ credentials: "omit"
740
+ };
741
+ if (options === null || options === void 0 ? void 0 : options.keepalive) init.keepalive = true;
742
+ const response = await fetch(url, init);
743
+ if (!response.ok) throw new FireflyTransportError(`Firefly: 上报失败,HTTP ${response.status}`, {
744
+ statusCode: response.status,
745
+ retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
746
+ });
747
+ return;
748
+ }
749
+ await sendWithXhr(url, body);
750
+ } catch (error) {
751
+ if (error instanceof FireflyTransportError) throw error;
752
+ throw new FireflyTransportError("Firefly: 上报网络错误", { cause: error });
753
+ }
754
+ },
755
+ /** 页面卸载阶段优先用 sendBeacon;浏览器拒绝时返回 false 走 fetch。 */
756
+ sendBeacon(body, dsn) {
757
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
758
+ try {
759
+ return navigator.sendBeacon(`${baseUrl}?key=${encodeURIComponent(dsn)}`, body);
760
+ } catch (_unused) {
761
+ return false;
762
+ }
763
+ }
764
+ };
765
+ }
766
+ /** XHR 兜底实现。仅在 fetch 不存在时使用。 */
767
+ function sendWithXhr(url, body) {
768
+ return new Promise((resolve, reject) => {
769
+ try {
770
+ const xhr = new XMLHttpRequest();
771
+ xhr.open("POST", url, true);
772
+ xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
773
+ xhr.onload = () => {
774
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
775
+ else reject(new FireflyTransportError(`Firefly: 上报失败,HTTP ${xhr.status}`, { statusCode: xhr.status }));
776
+ };
777
+ xhr.onerror = () => reject(new FireflyTransportError("Firefly: 上报网络错误"));
778
+ xhr.send(body);
779
+ } catch (error) {
780
+ reject(new FireflyTransportError("Firefly: 上报网络错误", { cause: error }));
781
+ }
782
+ });
783
+ }
784
+ // src/index.ts
785
+ /**
786
+ * 浏览器端入口。
787
+ *
788
+ * 一行接入:
789
+ * ```ts
790
+ * const handle = initFirefly({ dsn, endpoint, environment, release });
791
+ * ```
792
+ * 之后未捕获异常、未处理的 Promise 拒绝、资源加载失败会自动上报;
793
+ * 失败 HTTP 请求会单独上报,成功请求进入页面请求日志,慢请求额外
794
+ * 写 warning 面包屑;点击、路由、console、生命周期和失败请求会写入
795
+ * 面包屑时间线。
796
+ */
797
+ function initFirefly(options) {
798
+ var _window$location, _window$location$sear, _window$location2, _window$location3, _window$location4;
799
+ const globalWindow = window;
800
+ if (globalWindow.__firefly__) return globalWindow.__firefly__;
801
+ const resolved = resolveOptions(options);
802
+ const client = createClient(resolved, {
803
+ storage: createStorage(),
804
+ transport: createTransport(resolved.endpoint),
805
+ context: createContext()
806
+ });
807
+ const initialPath = ((_window$location = window.location) === null || _window$location === void 0 ? void 0 : _window$location.pathname) || "/";
808
+ const initialQuery = queryOf((_window$location$sear = (_window$location2 = window.location) === null || _window$location2 === void 0 ? void 0 : _window$location2.search) !== null && _window$location$sear !== void 0 ? _window$location$sear : "");
809
+ client.startPageSession(initialPath, _objectSpread2({
810
+ navigation_type: "initial",
811
+ url: (_window$location3 = window.location) === null || _window$location3 === void 0 ? void 0 : _window$location3.href
812
+ }, initialQuery ? { query: initialQuery } : {}));
813
+ client.addBreadcrumb({
814
+ type: "navigation",
815
+ category: "page",
816
+ level: "info",
817
+ timestamp: Date.now(),
818
+ message: `进入 ${initialPath}`,
819
+ data: {
820
+ path: initialPath,
821
+ url: (_window$location4 = window.location) === null || _window$location4 === void 0 ? void 0 : _window$location4.href,
822
+ navigation_type: "initial"
823
+ }
824
+ });
825
+ const disposeHooks = [
826
+ installGlobalErrorHooks(client),
827
+ ...options.captureResourceErrors === false ? [] : [installResourceErrorHooks(client)],
828
+ ...options.captureClicks === false ? [] : [installClickBreadcrumbs(client)],
829
+ installConsoleBreadcrumbs(client, options),
830
+ installLifecycleBreadcrumbs(client, options),
831
+ installHttpErrorHooks(client, options),
832
+ installPageHideFlush(client)
833
+ ];
834
+ const handle = {
835
+ client,
836
+ dispose() {
837
+ for (const dispose of disposeHooks) dispose();
838
+ client.close();
839
+ if (globalWindow.__firefly__ === handle) delete globalWindow.__firefly__;
840
+ }
841
+ };
842
+ globalWindow.__firefly__ = handle;
843
+ return handle;
844
+ }
845
+ /** 把 location.search 解析成可序列化的页面参数。 */
846
+ function queryOf(search) {
847
+ const params = new URLSearchParams(search);
848
+ const keys = [...new Set(params.keys())];
849
+ if (keys.length === 0) return void 0;
850
+ const result = {};
851
+ for (const key of keys) {
852
+ const values = params.getAll(key);
853
+ if (values.length === 0) continue;
854
+ result[key] = values.length === 1 ? values[0] : values;
855
+ }
856
+ return Object.keys(result).length > 0 ? result : void 0;
857
+ }
858
+ /**
859
+ * 页面隐藏/卸载时冲刷一次队列。
860
+ * 移动端切后台、关闭标签页都会触发 pagehide,这是最后的补发机会。
861
+ */
862
+ function installPageHideFlush(client) {
863
+ const handler = () => {
864
+ client.flush({
865
+ beacon: true,
866
+ keepalive: true
867
+ });
868
+ };
869
+ window.addEventListener("pagehide", handler);
870
+ return () => {
871
+ window.removeEventListener("pagehide", handler);
872
+ };
873
+ }
874
+ export { initFirefly };
875
+
876
+ //# sourceMappingURL=index.js.map