@fz-firefly/core 0.0.0-stage → 0.2.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/dist/index.cjs ADDED
@@ -0,0 +1,1585 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/typeof.js
3
+ function _typeof(o) {
4
+ "@babel/helpers - typeof";
5
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
6
+ return typeof o;
7
+ } : function(o) {
8
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
9
+ }, _typeof(o);
10
+ }
11
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPrimitive.js
12
+ function toPrimitive(t, r) {
13
+ if ("object" != _typeof(t) || !t) return t;
14
+ var e = t[Symbol.toPrimitive];
15
+ if (void 0 !== e) {
16
+ var i = e.call(t, r || "default");
17
+ if ("object" != _typeof(i)) return i;
18
+ throw new TypeError("@@toPrimitive must return a primitive value.");
19
+ }
20
+ return ("string" === r ? String : Number)(t);
21
+ }
22
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPropertyKey.js
23
+ function toPropertyKey(t) {
24
+ var i = toPrimitive(t, "string");
25
+ return "symbol" == _typeof(i) ? i : i + "";
26
+ }
27
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/defineProperty.js
28
+ function _defineProperty(e, r, t) {
29
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
30
+ value: t,
31
+ enumerable: !0,
32
+ configurable: !0,
33
+ writable: !0
34
+ }) : e[r] = t, e;
35
+ }
36
+ // src/breadcrumbs/store.ts
37
+ /**
38
+ * 面包屑环形缓冲。
39
+ * 只保留最近 N 条,超出丢最旧的。错误发生时快照一份塞进事件。
40
+ */
41
+ var BreadcrumbStore = class {
42
+ constructor(max) {
43
+ this.max = max;
44
+ _defineProperty(this, "items", []);
45
+ _defineProperty(this, "dropped", 0);
46
+ }
47
+ /** 追加一条面包屑,并裁剪到上限。 */
48
+ add(breadcrumb) {
49
+ this.items.push(breadcrumb);
50
+ if (this.items.length > this.max) {
51
+ const removed = this.items.length - this.max;
52
+ this.items.splice(0, removed);
53
+ this.dropped += removed;
54
+ }
55
+ }
56
+ /** 返回副本,避免调用方修改内部数组。 */
57
+ snapshot() {
58
+ return [...this.items];
59
+ }
60
+ /** 当前缓冲状态,用于提示时间线可能不完整。 */
61
+ stats() {
62
+ return {
63
+ size: this.items.length,
64
+ max: this.max,
65
+ dropped: this.dropped
66
+ };
67
+ }
68
+ };
69
+ // src/utils/uuid.ts
70
+ /**
71
+ * 生成事件 id(uuid v4)。
72
+ *
73
+ * 优先用平台自带的 crypto.randomUUID;
74
+ * 老浏览器与部分小程序没有它,退化为 Math.random 版本。
75
+ * 这个 id 是服务端的幂等键,重复概率必须足够低。
76
+ */
77
+ function createEventId() {
78
+ const cryptoApi = typeof globalThis === "undefined" ? void 0 : globalThis.crypto;
79
+ if (typeof (cryptoApi === null || cryptoApi === void 0 ? void 0 : cryptoApi.randomUUID) === "function") return cryptoApi.randomUUID();
80
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
81
+ const random = Math.random() * 16 | 0;
82
+ return (char === "x" ? random : random & 3 | 8).toString(16);
83
+ });
84
+ }
85
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/objectSpread2.js
86
+ function ownKeys(e, r) {
87
+ var t = Object.keys(e);
88
+ if (Object.getOwnPropertySymbols) {
89
+ var o = Object.getOwnPropertySymbols(e);
90
+ r && (o = o.filter(function(r) {
91
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
92
+ })), t.push.apply(t, o);
93
+ }
94
+ return t;
95
+ }
96
+ function _objectSpread2(e) {
97
+ for (var r = 1; r < arguments.length; r++) {
98
+ var t = null != arguments[r] ? arguments[r] : {};
99
+ r % 2 ? ownKeys(Object(t), !0).forEach(function(r) {
100
+ _defineProperty(e, r, t[r]);
101
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r) {
102
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
103
+ });
104
+ }
105
+ return e;
106
+ }
107
+ // src/runtime/page-session.ts
108
+ /**
109
+ * 页面会话跟踪器。
110
+ *
111
+ * 一次「进入页面」对应一个 session:
112
+ * - Web:首次加载 + SPA 路由切换;
113
+ * - uni-app / Taro:navigateTo / redirectTo / switchTab / reLaunch / navigateBack。
114
+ *
115
+ * 面包屑会自动带上 session_id、seq、t_rel_ms,事件里则通过
116
+ * `contexts.page` 暴露会话信息,后台可以把一次页面访问内的
117
+ * 请求、点击、报错串成一条时间线。
118
+ */
119
+ var PageSessionTracker = class {
120
+ constructor() {
121
+ _defineProperty(this, "id", createEventId());
122
+ _defineProperty(this, "path", void 0);
123
+ _defineProperty(this, "startedAt", Date.now());
124
+ _defineProperty(this, "seq", 0);
125
+ _defineProperty(this, "meta", {});
126
+ }
127
+ /** 开始一次新的页面会话。 */
128
+ start(path, meta = {}) {
129
+ this.id = createEventId();
130
+ this.path = path;
131
+ this.startedAt = Date.now();
132
+ this.seq = 0;
133
+ this.meta = _objectSpread2({}, meta);
134
+ return this.info();
135
+ }
136
+ /** 分配下一个面包屑序号。 */
137
+ nextSeq() {
138
+ this.seq += 1;
139
+ return this.seq;
140
+ }
141
+ /** 距离页面进入时刻的毫秒数。 */
142
+ get elapsedMs() {
143
+ return Math.max(0, Date.now() - this.startedAt);
144
+ }
145
+ /** 当前会话快照。 */
146
+ info() {
147
+ return {
148
+ id: this.id,
149
+ path: this.path,
150
+ startedAt: this.startedAt,
151
+ seq: this.seq,
152
+ meta: _objectSpread2({}, this.meta)
153
+ };
154
+ }
155
+ /** 事件 contexts.page 的内容。 */
156
+ context() {
157
+ return _objectSpread2(_objectSpread2(_objectSpread2({}, this.meta), {}, { session_id: this.id }, this.path ? { path: this.path } : {}), {}, { started_at: new Date(this.startedAt).toISOString() });
158
+ }
159
+ };
160
+ // src/utils/text.ts
161
+ /** 截断标记;保留头尾时插在中间。 */
162
+ const TRUNCATED_MARKER = "\n…[truncated]…\n";
163
+ /**
164
+ * 按字符数压缩文本,超长时保留头部和尾部。
165
+ *
166
+ * 错误信息、HTML 错误页和 JSON 的尾部经常比中间更重要,单纯 slice(0, max)
167
+ * 会把真正的错误码/错误提示丢掉。
168
+ */
169
+ function truncateWithTail(value, max) {
170
+ if (value.length <= max) return {
171
+ value,
172
+ truncated: false
173
+ };
174
+ if (max <= 15) return {
175
+ value: value.slice(0, max),
176
+ truncated: true
177
+ };
178
+ const tail = Math.min(256, Math.floor(max / 4));
179
+ const head = Math.max(0, max - tail - 15);
180
+ return {
181
+ value: `${value.slice(0, head)}${TRUNCATED_MARKER}${value.slice(-tail)}`,
182
+ truncated: true
183
+ };
184
+ }
185
+ // src/runtime/request-log.ts
186
+ /**
187
+ * 页面会话内的请求日志。
188
+ *
189
+ * 只保留最近 N 条,避免事件体积失控;成功请求也记录,
190
+ * 但不会像面包屑那样把点击/导航挤出 50 条缓冲。
191
+ */
192
+ var RequestLogStore = class {
193
+ constructor(max, maxBodyChars = 1024) {
194
+ this.max = max;
195
+ this.maxBodyChars = maxBodyChars;
196
+ _defineProperty(this, "items", []);
197
+ _defineProperty(this, "total", 0);
198
+ _defineProperty(this, "failed", 0);
199
+ }
200
+ /** 追加一条请求记录。 */
201
+ add(record) {
202
+ this.total += 1;
203
+ if (!record.ok) this.failed += 1;
204
+ this.items.push(this.compact(record));
205
+ if (this.items.length > this.max) this.items.splice(0, this.items.length - this.max);
206
+ }
207
+ /** 进入新页面时清空,避免把上一个页面的请求带进来。 */
208
+ clear() {
209
+ this.items = [];
210
+ this.total = 0;
211
+ this.failed = 0;
212
+ }
213
+ /** 返回副本。 */
214
+ snapshot() {
215
+ return {
216
+ items: [...this.items],
217
+ total: this.total,
218
+ failed: this.failed
219
+ };
220
+ }
221
+ /** 请求日志不需要完整响应体,截断到较小长度,避免 30 条撑爆信封。 */
222
+ compact(record) {
223
+ const requestBody = truncateValue(record.request_body, this.maxBodyChars);
224
+ const responseBody = truncateValue(record.response_body, this.maxBodyChars);
225
+ return _objectSpread2(_objectSpread2(_objectSpread2({}, record), record.request_body !== void 0 ? {
226
+ request_body: requestBody.value,
227
+ request_body_truncated: record.request_body_truncated || requestBody.truncated
228
+ } : {}), record.response_body !== void 0 ? {
229
+ response_body: responseBody.value,
230
+ response_body_truncated: record.response_body_truncated || responseBody.truncated
231
+ } : {});
232
+ }
233
+ };
234
+ /** 字符串直接截断;对象序列化后超长则退化成截断字符串。 */
235
+ function truncateValue(value, maxChars) {
236
+ if (value === void 0) return {
237
+ value,
238
+ truncated: false
239
+ };
240
+ if (typeof value === "string") return truncateWithTail(value, maxChars);
241
+ try {
242
+ const json = JSON.stringify(value);
243
+ if (!json || json.length <= maxChars) return {
244
+ value,
245
+ truncated: false
246
+ };
247
+ return {
248
+ value: truncateWithTail(json, maxChars).value,
249
+ truncated: true
250
+ };
251
+ } catch (_unused) {
252
+ return {
253
+ value: "[Unserializable]",
254
+ truncated: true
255
+ };
256
+ }
257
+ }
258
+ // src/runtime/normalize.ts
259
+ /**
260
+ * 协议上限的本地副本。
261
+ *
262
+ * core 不运行时 import shared(避免为一个常量把 Zod 协议打进 core),
263
+ * 这些值必须与 packages/shared/src/constants.ts 保持一致。
264
+ */
265
+ const MAX_FIELD_CHARS = 16384;
266
+ const MAX_URL_CHARS = 2048;
267
+ const MAX_FRAMES = 100;
268
+ const MAX_OCCURRENCES = 1e4;
269
+ const MAX_RELEASE_CHARS = 128;
270
+ const MAX_DIST_CHARS = 128;
271
+ const MAX_ROUTE_CHARS = 512;
272
+ const MAX_USER_ID_CHARS = 256;
273
+ const MAX_BREADCRUMB_TYPE_CHARS = 32;
274
+ const MAX_BREADCRUMB_CATEGORY_CHARS = 64;
275
+ const MAX_BREADCRUMB_MESSAGE_CHARS = 512;
276
+ const MAX_SESSION_ID_CHARS = 64;
277
+ const MAX_EXCEPTION_TYPE_CHARS = 256;
278
+ const MAX_FUNCTION_CHARS = 512;
279
+ const MAX_EVENT_NAME_CHARS = 128;
280
+ /** 按字符数截断;调用方传入的都是 UTF-16 字符串。 */
281
+ function cut(value, max) {
282
+ return value.length <= max ? value : value.slice(0, max);
283
+ }
284
+ function cutOptional(value, max) {
285
+ return value === void 0 ? void 0 : cut(value, max);
286
+ }
287
+ function normalizeFrame(frame) {
288
+ return _objectSpread2(_objectSpread2({}, frame), {}, {
289
+ filename: cut(frame.filename, MAX_URL_CHARS),
290
+ abs_path: cutOptional(frame.abs_path, MAX_URL_CHARS),
291
+ function: cutOptional(frame.function, MAX_FUNCTION_CHARS)
292
+ });
293
+ }
294
+ /** 超过 100 帧时保留首尾各 50 帧,中间部分丢失。 */
295
+ function normalizeFrames(frames) {
296
+ if (frames.length <= MAX_FRAMES) return frames.map(normalizeFrame);
297
+ const half = Math.floor(MAX_FRAMES / 2);
298
+ return [...frames.slice(0, half), ...frames.slice(-50)].map(normalizeFrame);
299
+ }
300
+ function normalizeBreadcrumb(breadcrumb) {
301
+ return _objectSpread2(_objectSpread2({}, breadcrumb), {}, {
302
+ type: cut(breadcrumb.type, MAX_BREADCRUMB_TYPE_CHARS),
303
+ category: cutOptional(breadcrumb.category, MAX_BREADCRUMB_CATEGORY_CHARS),
304
+ session_id: cutOptional(breadcrumb.session_id, MAX_SESSION_ID_CHARS),
305
+ message: cutOptional(breadcrumb.message, MAX_BREADCRUMB_MESSAGE_CHARS)
306
+ });
307
+ }
308
+ function normalizeOccurrences(value) {
309
+ if (!Number.isFinite(value)) return 1;
310
+ return Math.min(Math.max(1, Math.floor(value)), MAX_OCCURRENCES);
311
+ }
312
+ /**
313
+ * 把事件裁剪到服务端协议允许的范围。
314
+ *
315
+ * 客户端不裁剪时,服务端 parseEnvelope 会静默丢弃非法 item,调用方却拿到 200。
316
+ * 这里统一处理字符串、帧数、面包屑、occurrences,并对空 message / event_name
317
+ * 返回 null,让调用方明确拒绝。
318
+ */
319
+ function normalizeEventItem(item) {
320
+ const base = _objectSpread2(_objectSpread2({}, item), {}, {
321
+ occurrences: normalizeOccurrences(item.occurrences),
322
+ release: cutOptional(item.release, MAX_RELEASE_CHARS),
323
+ dist: cutOptional(item.dist, MAX_DIST_CHARS),
324
+ breadcrumbs: item.breadcrumbs.slice(-50).map(normalizeBreadcrumb),
325
+ url: cutOptional(item.url, MAX_URL_CHARS),
326
+ route: cutOptional(item.route, MAX_ROUTE_CHARS),
327
+ user: item.user ? {
328
+ id: cutOptional(item.user.id, MAX_USER_ID_CHARS),
329
+ anonymous_id: cutOptional(item.user.anonymous_id, MAX_USER_ID_CHARS)
330
+ } : void 0
331
+ });
332
+ if (base.type === "error") {
333
+ const exception = base.exception;
334
+ const stacktrace = exception.stacktrace ? { frames: normalizeFrames(exception.stacktrace.frames) } : void 0;
335
+ return _objectSpread2(_objectSpread2({}, base), {}, {
336
+ message: cutOptional(base.message, MAX_FIELD_CHARS),
337
+ exception: _objectSpread2(_objectSpread2({}, exception), {}, {
338
+ type: cut(exception.type, MAX_EXCEPTION_TYPE_CHARS),
339
+ value: cutOptional(exception.value, MAX_FIELD_CHARS)
340
+ }, stacktrace ? { stacktrace } : {})
341
+ });
342
+ }
343
+ if (base.type === "message") {
344
+ if (base.message.trim() === "") return null;
345
+ return _objectSpread2(_objectSpread2({}, base), {}, { message: cut(base.message, MAX_FIELD_CHARS) });
346
+ }
347
+ if (base.type === "track") {
348
+ if (base.event_name.trim() === "") return null;
349
+ return _objectSpread2(_objectSpread2({}, base), {}, { event_name: cut(base.event_name, MAX_EVENT_NAME_CHARS) });
350
+ }
351
+ return base;
352
+ }
353
+ // src/options.ts
354
+ /** 默认自动冲刷间隔:5 秒。与客户端节流窗口一致。 */
355
+ const DEFAULT_FLUSH_INTERVAL_MS = 5e3;
356
+ /** 默认内存队列上限。 */
357
+ const DEFAULT_MAX_QUEUE_SIZE = 100;
358
+ /** 默认面包屑上限。 */
359
+ const DEFAULT_MAX_BREADCRUMBS = 50;
360
+ /** 默认事件携带的请求日志条数。 */
361
+ const DEFAULT_MAX_REQUEST_LOG = 30;
362
+ /** 默认慢请求阈值:1 秒。 */
363
+ const DEFAULT_SLOW_REQUEST_THRESHOLD_MS = 1e3;
364
+ /** 默认响应体最大字符数。 */
365
+ const DEFAULT_MAX_HTTP_RESPONSE_CHARS = 4096;
366
+ /** 默认单批次最大重试次数。 */
367
+ const DEFAULT_MAX_RETRIES = 5;
368
+ /** 默认重试基础延迟:5 秒。 */
369
+ const DEFAULT_RETRY_BASE_DELAY_MS = 5e3;
370
+ /** 默认重试最大延迟:5 分钟。 */
371
+ const DEFAULT_RETRY_MAX_DELAY_MS = 3e5;
372
+ /**
373
+ * 校验必填项并补齐默认值。
374
+ *
375
+ * 这里故意抛错而不是静默降级:dsn 或 endpoint 缺失时,
376
+ * 上报永远不会成功,早点暴露比后台一条数据都没有更好。
377
+ */
378
+ function resolveOptions(options) {
379
+ var _options$flushInterva, _options$maxQueueSize, _options$maxRequestLo, _options$slowRequestT, _options$maxHttpRespo, _options$maxRetries, _options$retryBaseDel, _options$retryMaxDela;
380
+ if (!options.dsn) throw new Error("Firefly: dsn 不能为空,请在后台项目设置中获取");
381
+ if (!options.endpoint) throw new Error("Firefly: endpoint 不能为空,请填写服务端地址");
382
+ if (!options.environment) throw new Error("Firefly: environment 不能为空,请传入构建环境");
383
+ return _objectSpread2(_objectSpread2({}, options), {}, {
384
+ flushIntervalMs: (_options$flushInterva = options.flushIntervalMs) !== null && _options$flushInterva !== void 0 ? _options$flushInterva : DEFAULT_FLUSH_INTERVAL_MS,
385
+ maxQueueSize: (_options$maxQueueSize = options.maxQueueSize) !== null && _options$maxQueueSize !== void 0 ? _options$maxQueueSize : 100,
386
+ maxBreadcrumbs: Number.isFinite(options.maxBreadcrumbs) ? Math.min(Math.max(0, Math.floor(options.maxBreadcrumbs)), 50) : 50,
387
+ maxRequestLog: (_options$maxRequestLo = options.maxRequestLog) !== null && _options$maxRequestLo !== void 0 ? _options$maxRequestLo : 30,
388
+ slowRequestThresholdMs: (_options$slowRequestT = options.slowRequestThresholdMs) !== null && _options$slowRequestT !== void 0 ? _options$slowRequestT : DEFAULT_SLOW_REQUEST_THRESHOLD_MS,
389
+ maxHttpResponseChars: (_options$maxHttpRespo = options.maxHttpResponseChars) !== null && _options$maxHttpRespo !== void 0 ? _options$maxHttpRespo : DEFAULT_MAX_HTTP_RESPONSE_CHARS,
390
+ maxRetries: (_options$maxRetries = options.maxRetries) !== null && _options$maxRetries !== void 0 ? _options$maxRetries : 5,
391
+ retryBaseDelayMs: (_options$retryBaseDel = options.retryBaseDelayMs) !== null && _options$retryBaseDel !== void 0 ? _options$retryBaseDel : DEFAULT_RETRY_BASE_DELAY_MS,
392
+ retryMaxDelayMs: (_options$retryMaxDela = options.retryMaxDelayMs) !== null && _options$retryMaxDela !== void 0 ? _options$retryMaxDela : DEFAULT_RETRY_MAX_DELAY_MS
393
+ });
394
+ }
395
+ // src/runtime/queue.ts
396
+ /** 队列在存储里的键名。加项目前缀由适配层负责(本包只有一个队列)。 */
397
+ const STORAGE_KEY = "firefly:queue";
398
+ /** 单个信封最多携带的事件数,与协议上限一致。 */
399
+ const BATCH_SIZE = 50;
400
+ /**
401
+ * 单个信封的最大字节数。
402
+ *
403
+ * 与 @fz-firefly/shared 的 LIMITS.MAX_BODY_BYTES 保持一致;
404
+ * 这里不 import shared,避免 core 为了一个常量引入协议/Zod 运行时。
405
+ */
406
+ const MAX_ENVELOPE_BYTES = 262144;
407
+ /** fetch keepalive 的保守上限;浏览器通常限制在 64KB 左右。 */
408
+ const MAX_KEEPALIVE_BYTES = 61440;
409
+ /** 计算 UTF-8 字节数,兼容没有 TextEncoder 的小程序/老 WebView。 */
410
+ function utf8ByteLength(value) {
411
+ let bytes = 0;
412
+ for (let index = 0; index < value.length; index += 1) {
413
+ const code = value.charCodeAt(index);
414
+ if (code < 128) bytes += 1;
415
+ else if (code < 2048) bytes += 2;
416
+ else if (code >= 55296 && code <= 56319) {
417
+ const next = value.charCodeAt(index + 1);
418
+ if (next >= 56320 && next <= 57343) {
419
+ bytes += 4;
420
+ index += 1;
421
+ } else bytes += 3;
422
+ } else bytes += 3;
423
+ }
424
+ return bytes;
425
+ }
426
+ /**
427
+ * 事件队列。
428
+ *
429
+ * 职责:
430
+ * - 入队时持久化,页面崩溃/刷新后下次启动还能补发;
431
+ * - 按 50 条一批发送;
432
+ * - 网络错误 / 5xx / 429 指数退避重试,超过上限丢弃该批次;
433
+ * - 4xx(401/403 除外)直接丢弃该批次,不无限重试;
434
+ * - 超过上限丢最旧的,防止错误风暴把存储撑爆。
435
+ */
436
+ var EventQueue = class {
437
+ constructor(deps) {
438
+ var _deps$maxRetries, _deps$retryBaseDelayM, _deps$retryMaxDelayMs, _deps$now, _deps$random;
439
+ this.deps = deps;
440
+ _defineProperty(this, "items", []);
441
+ _defineProperty(this, "retryAttempt", 0);
442
+ _defineProperty(this, "nextRetryAt", 0);
443
+ _defineProperty(this, "flushPromise", null);
444
+ _defineProperty(this, "maxRetries", void 0);
445
+ _defineProperty(this, "retryBaseDelayMs", void 0);
446
+ _defineProperty(this, "retryMaxDelayMs", void 0);
447
+ _defineProperty(this, "now", void 0);
448
+ _defineProperty(this, "random", void 0);
449
+ this.maxRetries = (_deps$maxRetries = deps.maxRetries) !== null && _deps$maxRetries !== void 0 ? _deps$maxRetries : 5;
450
+ this.retryBaseDelayMs = (_deps$retryBaseDelayM = deps.retryBaseDelayMs) !== null && _deps$retryBaseDelayM !== void 0 ? _deps$retryBaseDelayM : DEFAULT_RETRY_BASE_DELAY_MS;
451
+ this.retryMaxDelayMs = (_deps$retryMaxDelayMs = deps.retryMaxDelayMs) !== null && _deps$retryMaxDelayMs !== void 0 ? _deps$retryMaxDelayMs : DEFAULT_RETRY_MAX_DELAY_MS;
452
+ this.now = (_deps$now = deps.now) !== null && _deps$now !== void 0 ? _deps$now : Date.now;
453
+ this.random = (_deps$random = deps.random) !== null && _deps$random !== void 0 ? _deps$random : Math.random;
454
+ this.restore();
455
+ }
456
+ /** 当前待发送条数。 */
457
+ get size() {
458
+ return this.items.length;
459
+ }
460
+ /** 入队并落盘。 */
461
+ enqueue(item) {
462
+ if (this.deps.maxQueueSize > 0 && this.items.length >= this.deps.maxQueueSize) this.items.shift();
463
+ this.items.push(item);
464
+ this.persist();
465
+ }
466
+ /**
467
+ * 冲刷队列。
468
+ * 同一时间只允许一个 flush 在跑,避免定时器、pagehide、手动 flush 并发。
469
+ * 这个方法不抛错,失败通过 onError 暴露。
470
+ */
471
+ flush(options = {}) {
472
+ if (this.flushPromise) return this.flushPromise.then(() => this.flush(options), () => this.flush(options));
473
+ const running = this.flushInternal(options).then(() => {
474
+ this.flushPromise = null;
475
+ }, (error) => {
476
+ this.flushPromise = null;
477
+ throw error;
478
+ });
479
+ this.flushPromise = running;
480
+ return running;
481
+ }
482
+ async flushInternal(options) {
483
+ const now = this.now();
484
+ if (now < this.nextRetryAt) return;
485
+ let batchLimit = BATCH_SIZE;
486
+ while (this.items.length > 0) {
487
+ let batch;
488
+ try {
489
+ batch = this.buildBatch(batchLimit);
490
+ } catch (error) {
491
+ const item = this.items[0];
492
+ if (!item) return;
493
+ this.removeItems([item]);
494
+ this.reportError(error);
495
+ continue;
496
+ }
497
+ if (!batch) {
498
+ const item = this.items[0];
499
+ if (!item) return;
500
+ this.removeItems([item]);
501
+ this.reportError(/* @__PURE__ */ new Error("Firefly: 单条事件超过信封上限,已丢弃"));
502
+ continue;
503
+ }
504
+ try {
505
+ let beaconAccepted = false;
506
+ if (options.beacon && this.deps.transport.sendBeacon) try {
507
+ beaconAccepted = this.deps.transport.sendBeacon(batch.body, this.deps.dsn);
508
+ } catch (_unused) {
509
+ beaconAccepted = false;
510
+ }
511
+ if (!beaconAccepted) {
512
+ const keepalive = options.keepalive === true && utf8ByteLength(batch.body) <= MAX_KEEPALIVE_BYTES;
513
+ await this.deps.transport.send(batch.body, this.deps.dsn, keepalive ? { keepalive: true } : void 0);
514
+ }
515
+ this.retryAttempt = 0;
516
+ this.nextRetryAt = 0;
517
+ this.removeItems(batch.items);
518
+ } catch (error) {
519
+ var _details$retryable, _details$retryAfterMs;
520
+ const details = error;
521
+ if (details.statusCode === 413) {
522
+ if (batch.items.length > 1) {
523
+ batchLimit = Math.max(1, Math.floor(batch.items.length / 2));
524
+ continue;
525
+ }
526
+ this.reportError(error);
527
+ this.removeItems(batch.items);
528
+ this.retryAttempt = 0;
529
+ this.nextRetryAt = 0;
530
+ continue;
531
+ }
532
+ this.reportError(error);
533
+ if (!((_details$retryable = details.retryable) !== null && _details$retryable !== void 0 ? _details$retryable : true)) {
534
+ this.removeItems(batch.items);
535
+ if (details.statusCode === 401 || details.statusCode === 403) {
536
+ this.items = [];
537
+ this.persist();
538
+ }
539
+ this.retryAttempt = 0;
540
+ this.nextRetryAt = 0;
541
+ return;
542
+ }
543
+ this.retryAttempt += 1;
544
+ if (this.retryAttempt > this.maxRetries) {
545
+ this.removeItems(batch.items);
546
+ this.retryAttempt = 0;
547
+ this.nextRetryAt = 0;
548
+ return;
549
+ }
550
+ const base = (_details$retryAfterMs = details.retryAfterMs) !== null && _details$retryAfterMs !== void 0 ? _details$retryAfterMs : this.retryBaseDelayMs * 2 ** (this.retryAttempt - 1);
551
+ const delay = Math.min(base, this.retryMaxDelayMs);
552
+ this.nextRetryAt = now + delay + delay * .2 * this.random();
553
+ return;
554
+ }
555
+ }
556
+ }
557
+ /**
558
+ * 选出不超过条数/字节上限的队首批次。
559
+ *
560
+ * 先按最大条数二进制搜索,buildEnvelope 每次都包含 sdk/sent_at 等
561
+ * 信封开销,所以用最终 JSON 的字节数判断,而不是估算 item 大小。
562
+ */
563
+ buildBatch(limit) {
564
+ const maxItems = Math.min(limit, BATCH_SIZE, this.items.length);
565
+ if (maxItems <= 0) return null;
566
+ let low = 1;
567
+ let high = maxItems;
568
+ let best = null;
569
+ while (low <= high) {
570
+ const count = Math.floor((low + high) / 2);
571
+ const items = this.items.slice(0, count);
572
+ const body = this.deps.buildEnvelope(items);
573
+ if (utf8ByteLength(body) <= MAX_ENVELOPE_BYTES) {
574
+ best = {
575
+ items,
576
+ body
577
+ };
578
+ low = count + 1;
579
+ } else high = count - 1;
580
+ }
581
+ return best;
582
+ }
583
+ /** 按对象引用移除已发送/已丢弃的条目,避免 flush 期间 enqueue 导致误删。 */
584
+ removeItems(items) {
585
+ if (items.length === 0) return;
586
+ const removing = new Set(items);
587
+ this.items = this.items.filter((item) => !removing.has(item));
588
+ this.persist();
589
+ }
590
+ /** 隔离用户的 onError,避免回调异常打断队列状态机。 */
591
+ reportError(error) {
592
+ try {
593
+ var _this$deps$onError, _this$deps;
594
+ (_this$deps$onError = (_this$deps = this.deps).onError) === null || _this$deps$onError === void 0 || _this$deps$onError.call(_this$deps, error);
595
+ } catch (_unused2) {}
596
+ }
597
+ /** 队列为空时删键,避免存储里留一个无意义的空数组。 */
598
+ persist() {
599
+ try {
600
+ if (this.items.length === 0) this.deps.storage.remove(STORAGE_KEY);
601
+ else this.deps.storage.set(STORAGE_KEY, JSON.stringify(this.items));
602
+ } catch (_unused3) {}
603
+ }
604
+ /** 启动时恢复上次未发完的条目。损坏的数据直接丢弃。 */
605
+ restore() {
606
+ try {
607
+ const raw = this.deps.storage.get(STORAGE_KEY);
608
+ if (!raw) return;
609
+ const parsed = JSON.parse(raw);
610
+ if (!Array.isArray(parsed)) return;
611
+ const max = this.deps.maxQueueSize > 0 ? this.deps.maxQueueSize : parsed.length;
612
+ this.items = parsed.slice(-max);
613
+ } catch (_unused4) {}
614
+ }
615
+ };
616
+ // src/runtime/scope.ts
617
+ /**
618
+ * 当前作用域。
619
+ * 保存 setUser / setTag / setContext 设置的值,组装事件时合并进去。
620
+ * 后续要支持 withScope 时,只需在这里做分支复制。
621
+ */
622
+ var Scope = class {
623
+ constructor() {
624
+ _defineProperty(
625
+ this,
626
+ /** 当前用户,null 表示未登录。 */
627
+ "user",
628
+ null
629
+ );
630
+ _defineProperty(
631
+ this,
632
+ /** 全局标签。 */
633
+ "tags",
634
+ {}
635
+ );
636
+ _defineProperty(
637
+ this,
638
+ /** 全局上下文,按类别分组。 */
639
+ "contexts",
640
+ {}
641
+ );
642
+ }
643
+ setUser(user) {
644
+ this.user = user;
645
+ }
646
+ setTag(key, value) {
647
+ this.tags[key] = value;
648
+ }
649
+ setContext(key, value) {
650
+ this.contexts[key] = value;
651
+ }
652
+ /** 合并一个上下文分组,保留平台层已经写入的字段。 */
653
+ mergeContext(key, value) {
654
+ var _this$contexts$key;
655
+ this.contexts[key] = _objectSpread2(_objectSpread2({}, (_this$contexts$key = this.contexts[key]) !== null && _this$contexts$key !== void 0 ? _this$contexts$key : {}), value);
656
+ }
657
+ };
658
+ // src/runtime/throttle.ts
659
+ /**
660
+ * 同签名错误节流。
661
+ *
662
+ * 错误风暴里同一个错误可能一秒触发上千次。如果每次都入队,
663
+ * 队列会被瞬间打满,真正有价值的新错误反而被挤掉。
664
+ * 这里以「5 秒固定窗口 + 签名」为粒度合并,只累加 occurrences。
665
+ */
666
+ var ErrorThrottle = class {
667
+ constructor(windowMs = 5e3, clock = Date.now) {
668
+ this.windowMs = windowMs;
669
+ this.clock = clock;
670
+ _defineProperty(this, "entries", /* @__PURE__ */ new Map());
671
+ }
672
+ /**
673
+ * 登记一条错误。
674
+ * @param item 已组装好的事件
675
+ * @param signature 同签名判定依据(类型 + 消息 + 首个业务帧)
676
+ */
677
+ register(item, signature) {
678
+ const now = this.clock();
679
+ const existing = this.entries.get(signature);
680
+ if (existing && now - existing.at < this.windowMs) return {
681
+ added: false,
682
+ existing: existing.item
683
+ };
684
+ this.entries.set(signature, {
685
+ item,
686
+ at: now
687
+ });
688
+ this.prune(now);
689
+ return { added: true };
690
+ }
691
+ /** 清掉过期条目,防止长时间运行后 Map 无限增长。 */
692
+ prune(now) {
693
+ if (this.entries.size < 500) return;
694
+ for (const [signature, entry] of this.entries) if (now - entry.at >= this.windowMs) this.entries.delete(signature);
695
+ }
696
+ };
697
+ // src/client.ts
698
+ /** SDK 自身标识,随每个信封上报,便于排查协议兼容问题。 */
699
+ const SDK_NAME = "@fz-firefly/core";
700
+ const SDK_VERSION = "0.2.0";
701
+ /** 匿名用户 id 的存储键;生成后不随登录变化。 */
702
+ const ANONYMOUS_ID_KEY = "firefly_anonymous_id";
703
+ /** 读取或生成稳定的匿名用户 id。 */
704
+ function resolveAnonymousId(storage) {
705
+ try {
706
+ const stored = storage.get(ANONYMOUS_ID_KEY);
707
+ if (stored) return stored;
708
+ } catch (_unused) {}
709
+ const created = `anon_${createEventId()}`;
710
+ try {
711
+ storage.set(ANONYMOUS_ID_KEY, created);
712
+ } catch (_unused2) {}
713
+ return created;
714
+ }
715
+ /**
716
+ * 合并平台上下文与 scope 上下文。
717
+ *
718
+ * 协议要求 contexts 的每个分组都是对象;这里顺便把平台层返回的
719
+ * 非对象值过滤掉,避免一条非法上下文导致整条事件被服务端拒绝。
720
+ */
721
+ function mergeContexts(base, extra) {
722
+ const result = {};
723
+ for (const [key, value] of Object.entries(base)) if (value && typeof value === "object" && !Array.isArray(value)) result[key] = value;
724
+ for (const [key, value] of Object.entries(extra)) {
725
+ var _result$key;
726
+ result[key] = _objectSpread2(_objectSpread2({}, (_result$key = result[key]) !== null && _result$key !== void 0 ? _result$key : {}), value);
727
+ }
728
+ return result;
729
+ }
730
+ /** 把长文本压到一行,避免异常面包屑本身过大。 */
731
+ function truncateText(value, max) {
732
+ return value.length <= max ? value : `${value.slice(0, max)}…`;
733
+ }
734
+ /**
735
+ * 创建客户端。
736
+ *
737
+ * 这里只做与平台无关的事情:组装事件、去重节流、排队、定时冲刷。
738
+ * 具体怎么发、存哪里、错误钩子挂在哪,全部由适配层注入。
739
+ */
740
+ function createClient(options, deps) {
741
+ const scope = new Scope();
742
+ const breadcrumbs = new BreadcrumbStore(options.maxBreadcrumbs);
743
+ const pageSession = new PageSessionTracker();
744
+ const requestLog = new RequestLogStore(options.maxRequestLog, Math.min(options.maxHttpResponseChars, 1024));
745
+ const throttle = new ErrorThrottle();
746
+ const anonymousId = resolveAnonymousId(deps.storage);
747
+ const queue = new EventQueue({
748
+ storage: deps.storage,
749
+ transport: deps.transport,
750
+ dsn: options.dsn,
751
+ maxQueueSize: options.maxQueueSize,
752
+ maxRetries: options.maxRetries,
753
+ retryBaseDelayMs: options.retryBaseDelayMs,
754
+ retryMaxDelayMs: options.retryMaxDelayMs,
755
+ onError: options.onError,
756
+ buildEnvelope: (items) => JSON.stringify({
757
+ sdk: {
758
+ name: SDK_NAME,
759
+ version: SDK_VERSION
760
+ },
761
+ sent_at: (/* @__PURE__ */ new Date()).toISOString(),
762
+ items
763
+ })
764
+ });
765
+ let timer = null;
766
+ if (options.flushIntervalMs > 0) {
767
+ var _unref;
768
+ timer = setInterval(() => {
769
+ queue.flush();
770
+ }, options.flushIntervalMs);
771
+ (_unref = timer.unref) === null || _unref === void 0 || _unref.call(timer);
772
+ }
773
+ /**
774
+ * 合并业务用户与 SDK 的匿名 id。
775
+ * 登录只增加 id,不会替换或重新生成 anonymous_id。
776
+ */
777
+ function resolveUser(override) {
778
+ var _ref, _base$anonymous_id;
779
+ const base = (_ref = override !== null && override !== void 0 ? override : scope.user) !== null && _ref !== void 0 ? _ref : {};
780
+ return _objectSpread2(_objectSpread2({}, base), {}, { anonymous_id: (_base$anonymous_id = base.anonymous_id) !== null && _base$anonymous_id !== void 0 ? _base$anonymous_id : anonymousId });
781
+ }
782
+ /** 追加面包屑,并自动补页面会话字段。 */
783
+ function addBreadcrumbInternal(breadcrumb) {
784
+ try {
785
+ const session = pageSession.info();
786
+ breadcrumbs.add(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({}, breadcrumb), breadcrumb.session_id ? {} : { session_id: session.id }), breadcrumb.seq !== void 0 ? {} : { seq: pageSession.nextSeq() }), breadcrumb.t_rel_ms !== void 0 ? {} : { t_rel_ms: pageSession.elapsedMs }));
787
+ } catch (_unused3) {}
788
+ }
789
+ /** 把上一次捕获的异常写成面包屑,方便看连续报错的时间线。 */
790
+ function addExceptionBreadcrumb(item) {
791
+ if (item.type !== "error") return;
792
+ try {
793
+ const exception = item.exception;
794
+ const value = exception.value ? truncateText(exception.value, 200) : "";
795
+ addBreadcrumbInternal({
796
+ type: "exception",
797
+ level: item.level,
798
+ timestamp: Date.now(),
799
+ message: value ? `${exception.type}: ${value}` : exception.type,
800
+ data: _objectSpread2({ type: exception.type }, value ? { value } : {})
801
+ });
802
+ } catch (_unused4) {}
803
+ }
804
+ /** 开始一次页面会话,并同步到事件上下文。 */
805
+ function startPageSession(path, meta = {}) {
806
+ const info = pageSession.start(path, meta);
807
+ requestLog.clear();
808
+ scope.setContext("page", pageSession.context());
809
+ return info;
810
+ }
811
+ /** 记录一次已完成请求;页面会话 id 自动补齐。 */
812
+ function recordRequest(record) {
813
+ try {
814
+ var _record$session_id;
815
+ requestLog.add(_objectSpread2(_objectSpread2({}, record), {}, { session_id: (_record$session_id = record.session_id) !== null && _record$session_id !== void 0 ? _record$session_id : pageSession.info().id }));
816
+ } catch (_unused5) {}
817
+ }
818
+ /** 组装 contexts,并按需带上当前页面会话的请求日志。 */
819
+ function buildContexts(includeRequestLog = false, extra = {}) {
820
+ const contexts = mergeContexts(deps.context.collect(), scope.contexts);
821
+ const breadcrumbStats = breadcrumbs.stats();
822
+ contexts.breadcrumb_stats = {
823
+ size: breadcrumbStats.size,
824
+ max: breadcrumbStats.max,
825
+ dropped: breadcrumbStats.dropped
826
+ };
827
+ if (includeRequestLog) {
828
+ const requests = requestLog.snapshot();
829
+ if (requests.total > 0) contexts.requests = {
830
+ items: requests.items,
831
+ total: requests.total,
832
+ failed: requests.failed
833
+ };
834
+ }
835
+ for (const [key, value] of Object.entries(extra)) {
836
+ var _contexts$key;
837
+ contexts[key] = _objectSpread2(_objectSpread2({}, (_contexts$key = contexts[key]) !== null && _contexts$key !== void 0 ? _contexts$key : {}), value);
838
+ }
839
+ return contexts;
840
+ }
841
+ /** 组装所有事件共用的字段。
842
+ * 每次调用都生成新的事件 id 与时间戳。
843
+ */
844
+ function buildBase(includeRequestLog = false) {
845
+ var _scope$user, _scope$user$anonymous, _scope$user2, _deps$context$pageUrl, _deps$context, _deps$context$pageRou, _deps$context2;
846
+ return {
847
+ event_id: createEventId(),
848
+ occurrences: 1,
849
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
850
+ platform: deps.context.platform,
851
+ environment: options.environment,
852
+ release: options.release,
853
+ dist: options.dist,
854
+ breadcrumbs: breadcrumbs.snapshot(),
855
+ contexts: buildContexts(includeRequestLog),
856
+ tags: _objectSpread2({}, scope.tags),
857
+ user: _objectSpread2(_objectSpread2({}, (_scope$user = scope.user) !== null && _scope$user !== void 0 ? _scope$user : {}), {}, { anonymous_id: (_scope$user$anonymous = (_scope$user2 = scope.user) === null || _scope$user2 === void 0 ? void 0 : _scope$user2.anonymous_id) !== null && _scope$user$anonymous !== void 0 ? _scope$user$anonymous : anonymousId }),
858
+ url: (_deps$context$pageUrl = (_deps$context = deps.context).pageUrl) === null || _deps$context$pageUrl === void 0 ? void 0 : _deps$context$pageUrl.call(_deps$context),
859
+ route: (_deps$context$pageRou = (_deps$context2 = deps.context).pageRoute) === null || _deps$context$pageRou === void 0 ? void 0 : _deps$context$pageRou.call(_deps$context2)
860
+ };
861
+ }
862
+ /** 把任意抛出物转成协议里的 exception 结构。 */
863
+ function normalizeError(error) {
864
+ if (error instanceof Error) return {
865
+ type: error.name || "Error",
866
+ value: error.message,
867
+ stacktrace: parseStack(error.stack)
868
+ };
869
+ if (typeof error === "string") return {
870
+ type: "Error",
871
+ value: error
872
+ };
873
+ try {
874
+ return {
875
+ type: "Error",
876
+ value: JSON.stringify(error)
877
+ };
878
+ } catch (_unused6) {
879
+ return {
880
+ type: "Error",
881
+ value: String(error)
882
+ };
883
+ }
884
+ }
885
+ /**
886
+ * 把 V8 / Gecko / JavaScriptCore 的 stack 字符串解析成帧数组。
887
+ *
888
+ * V8: at fn (https://a.com/app.js:1:2)
889
+ * Gecko: fn@https://a.com/app.js:1:2
890
+ * JSC: fn@https://a.com/app.js:1:2
891
+ *
892
+ * 不再无条件跳过第一行:Safari/Firefox 的第一行就是真实帧。
893
+ */
894
+ function parseStack(stack) {
895
+ if (!stack) return void 0;
896
+ const frames = [];
897
+ for (const line of stack.split(/\r?\n/)) {
898
+ const v8 = line.match(/^\s*at\s+(?:(.*?)\s+\()?(.*?):(\d+)(?::(\d+))?\)?\s*$/);
899
+ if (v8) {
900
+ const [, fn, filename, lineno, colno] = v8;
901
+ frames.push(makeFrame(fn, filename, lineno, colno));
902
+ continue;
903
+ }
904
+ const gecko = line.match(/^(.*?)@(.*?):(\d+)(?::(\d+))?$/);
905
+ if (gecko) {
906
+ const [, fn, filename, lineno, colno] = gecko;
907
+ frames.push(makeFrame(fn, filename, lineno, colno));
908
+ }
909
+ }
910
+ return frames.length > 0 ? { frames } : void 0;
911
+ }
912
+ function makeFrame(fn, filename, lineno, colno) {
913
+ const file = filename || "<unknown>";
914
+ return _objectSpread2(_objectSpread2({
915
+ filename: file,
916
+ function: fn ? fn.trim() || void 0 : void 0,
917
+ lineno: Number(lineno)
918
+ }, colno ? { colno: Number(colno) } : {}), {}, { in_app: !file.includes("node_modules") });
919
+ }
920
+ /** 客户端节流签名:类型 + 消息 + 首个业务帧。行号刻意不参与,与服务端一致。 */
921
+ function signatureOf(item) {
922
+ var _item$exception$stack, _item$exception$stack2, _item$exception$value, _first$function, _first$filename;
923
+ if (item.type !== "error") return `${item.type}:${item.event_id}`;
924
+ const first = ((_item$exception$stack = (_item$exception$stack2 = item.exception.stacktrace) === null || _item$exception$stack2 === void 0 ? void 0 : _item$exception$stack2.frames) !== null && _item$exception$stack !== void 0 ? _item$exception$stack : []).find((frame) => frame.in_app);
925
+ return `${item.exception.type}|${(_item$exception$value = item.exception.value) !== null && _item$exception$value !== void 0 ? _item$exception$value : ""}|${(_first$function = first === null || first === void 0 ? void 0 : first.function) !== null && _first$function !== void 0 ? _first$function : ""}@${(_first$filename = first === null || first === void 0 ? void 0 : first.filename) !== null && _first$filename !== void 0 ? _first$filename : ""}`;
926
+ }
927
+ /**
928
+ * 先按协议裁剪,再走 beforeSend,最后再裁剪一次。
929
+ *
930
+ * beforeSend 可能改大字段或返回缺字段的对象;第二次裁剪保证最终入队
931
+ * 的 item 仍然符合服务端 schema,避免被静默丢弃。
932
+ */
933
+ function prepareItem(item) {
934
+ const normalized = normalizeEventItem(item);
935
+ if (!normalized) return null;
936
+ const filtered = options.beforeSend ? options.beforeSend(normalized) : normalized;
937
+ if (!filtered) return null;
938
+ return normalizeEventItem(filtered);
939
+ }
940
+ /** 统一走 beforeSend 与队列,避免每种事件各写一遍。 */
941
+ function submit(item) {
942
+ const prepared = prepareItem(item);
943
+ if (prepared) queue.enqueue(prepared);
944
+ return prepared;
945
+ }
946
+ return {
947
+ captureException(error, context = {}) {
948
+ try {
949
+ var _context$level, _context$contexts, _context$tags;
950
+ const prepared = prepareItem(_objectSpread2(_objectSpread2({}, buildBase(true)), {}, {
951
+ type: "error",
952
+ category: "code",
953
+ level: (_context$level = context.level) !== null && _context$level !== void 0 ? _context$level : "error",
954
+ message: error instanceof Error ? error.message : String(error),
955
+ exception: normalizeError(error),
956
+ contexts: buildContexts(true, (_context$contexts = context.contexts) !== null && _context$contexts !== void 0 ? _context$contexts : {}),
957
+ tags: _objectSpread2(_objectSpread2({}, scope.tags), (_context$tags = context.tags) !== null && _context$tags !== void 0 ? _context$tags : {}),
958
+ user: resolveUser(context.user)
959
+ }));
960
+ if (!prepared) return "";
961
+ const result = throttle.register(prepared, signatureOf(prepared));
962
+ if (!result.added && result.existing) {
963
+ result.existing.occurrences = Math.min(result.existing.occurrences + 1, MAX_OCCURRENCES);
964
+ return result.existing.event_id;
965
+ }
966
+ queue.enqueue(prepared);
967
+ addExceptionBreadcrumb(prepared);
968
+ return prepared.event_id;
969
+ } catch (_unused7) {
970
+ return "";
971
+ }
972
+ },
973
+ captureMessage(message, level = "info", context = {}) {
974
+ try {
975
+ var _context$level2, _context$source, _context$contexts2, _context$tags2, _filtered$event_id;
976
+ const filtered = submit(_objectSpread2(_objectSpread2({}, buildBase(true)), {}, {
977
+ type: "message",
978
+ category: "interface",
979
+ level: (_context$level2 = context.level) !== null && _context$level2 !== void 0 ? _context$level2 : level,
980
+ source: (_context$source = context.source) !== null && _context$source !== void 0 ? _context$source : "message",
981
+ message,
982
+ contexts: buildContexts(true, (_context$contexts2 = context.contexts) !== null && _context$contexts2 !== void 0 ? _context$contexts2 : {}),
983
+ tags: _objectSpread2(_objectSpread2({}, scope.tags), (_context$tags2 = context.tags) !== null && _context$tags2 !== void 0 ? _context$tags2 : {}),
984
+ user: resolveUser(context.user)
985
+ }));
986
+ return (_filtered$event_id = filtered === null || filtered === void 0 ? void 0 : filtered.event_id) !== null && _filtered$event_id !== void 0 ? _filtered$event_id : "";
987
+ } catch (_unused8) {
988
+ return "";
989
+ }
990
+ },
991
+ track(name, properties = {}) {
992
+ try {
993
+ submit(_objectSpread2(_objectSpread2({}, buildBase(false)), {}, {
994
+ type: "track",
995
+ event_name: name,
996
+ properties
997
+ }));
998
+ } catch (_unused9) {}
999
+ },
1000
+ startPageSession,
1001
+ recordRequest,
1002
+ addBreadcrumb(breadcrumb) {
1003
+ addBreadcrumbInternal(breadcrumb);
1004
+ },
1005
+ setUser(user) {
1006
+ scope.setUser(user);
1007
+ },
1008
+ setTag(key, value) {
1009
+ scope.setTag(key, value);
1010
+ },
1011
+ setContext(key, value) {
1012
+ scope.setContext(key, value);
1013
+ },
1014
+ async flush(options) {
1015
+ try {
1016
+ await queue.flush(options);
1017
+ } catch (_unused10) {}
1018
+ },
1019
+ close() {
1020
+ if (timer) {
1021
+ clearInterval(timer);
1022
+ timer = null;
1023
+ }
1024
+ }
1025
+ };
1026
+ }
1027
+ // src/errors.ts
1028
+ /**
1029
+ * 上报失败错误。
1030
+ *
1031
+ * 队列需要根据 HTTP 状态判断是否值得重试:
1032
+ * - 网络错误、408、425、429、5xx:可以重试;
1033
+ * - 400、401、403、404、413、422 等:重试也不会成功,应该丢弃。
1034
+ */
1035
+ var FireflyTransportError = class extends Error {
1036
+ constructor(message, options = {}) {
1037
+ var _options$retryable;
1038
+ super(message);
1039
+ _defineProperty(this, "statusCode", void 0);
1040
+ _defineProperty(this, "retryable", void 0);
1041
+ _defineProperty(this, "retryAfterMs", void 0);
1042
+ _defineProperty(this, "cause", void 0);
1043
+ this.name = "FireflyTransportError";
1044
+ this.statusCode = options.statusCode;
1045
+ this.retryable = (_options$retryable = options.retryable) !== null && _options$retryable !== void 0 ? _options$retryable : isRetryableStatus(options.statusCode);
1046
+ this.retryAfterMs = options.retryAfterMs;
1047
+ this.cause = options.cause;
1048
+ }
1049
+ };
1050
+ /** 根据 HTTP 状态判断是否可重试。 */
1051
+ function isRetryableStatus(statusCode) {
1052
+ if (statusCode === void 0 || statusCode === 0) return true;
1053
+ if (statusCode === 408 || statusCode === 425 || statusCode === 429) return true;
1054
+ return statusCode >= 500;
1055
+ }
1056
+ /** 解析 Retry-After:支持秒数和 HTTP 日期两种格式。 */
1057
+ function parseRetryAfter(value) {
1058
+ if (!value) return void 0;
1059
+ const seconds = Number(value);
1060
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
1061
+ const date = Date.parse(value);
1062
+ if (!Number.isFinite(date)) return void 0;
1063
+ return Math.max(0, date - Date.now());
1064
+ }
1065
+ // src/http.ts
1066
+ /** 面包屑里单个 body 的上限;请求日志自身还有更小的限制。 */
1067
+ const MAX_BREADCRUMB_BODY_CHARS = 1024;
1068
+ /** 把过大的 body 压成 JSON 字符串片段,避免 50 条面包屑撑爆信封。 */
1069
+ function compactBody(value) {
1070
+ if (value === void 0) return {
1071
+ value,
1072
+ truncated: false
1073
+ };
1074
+ if (typeof value === "string") return truncateWithTail(value, MAX_BREADCRUMB_BODY_CHARS);
1075
+ try {
1076
+ const json = JSON.stringify(value);
1077
+ if (!json || json.length <= MAX_BREADCRUMB_BODY_CHARS) return {
1078
+ value,
1079
+ truncated: false
1080
+ };
1081
+ return {
1082
+ value: truncateWithTail(json, MAX_BREADCRUMB_BODY_CHARS).value,
1083
+ truncated: true
1084
+ };
1085
+ } catch (_unused) {
1086
+ return {
1087
+ value: "[Unserializable]",
1088
+ truncated: true
1089
+ };
1090
+ }
1091
+ }
1092
+ /**
1093
+ * 记录一次已完成请求。
1094
+ *
1095
+ * 所有请求都进入页面请求日志;只有失败请求或慢请求才写面包屑,
1096
+ * 避免 50 条面包屑缓冲被正常请求挤满。
1097
+ */
1098
+ function captureHttpRequest(client, details) {
1099
+ var _details$ok, _details$slowThreshol;
1100
+ const ok = (_details$ok = details.ok) !== null && _details$ok !== void 0 ? _details$ok : details.statusCode >= 200 && details.statusCode < 400;
1101
+ const statusLabel = details.statusCode > 0 ? String(details.statusCode) : "NETWORK";
1102
+ const requestBody = compactBody(details.requestBody);
1103
+ const responseBody = compactBody(details.responseBody);
1104
+ const requestBodyTruncated = details.requestBodyTruncated || requestBody.truncated;
1105
+ const responseBodyTruncated = details.responseBodyTruncated || responseBody.truncated;
1106
+ const data = _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({
1107
+ method: details.method,
1108
+ url: details.url,
1109
+ status_code: details.statusCode,
1110
+ duration_ms: details.durationMs,
1111
+ ok,
1112
+ phase: "end"
1113
+ }, details.requestId ? { request_id: details.requestId } : {}), details.responseSize !== void 0 ? { response_size: details.responseSize } : {}), details.requestBody !== void 0 ? { request_body: requestBody.value } : {}), requestBodyTruncated ? { request_body_truncated: true } : {}), details.responseBody !== void 0 ? { response_body: responseBody.value } : {}), responseBodyTruncated ? { response_body_truncated: true } : {}), details.responseHeaders ? { response_headers: details.responseHeaders } : {}), details.error ? { error: details.error } : {});
1114
+ try {
1115
+ var _client$recordRequest;
1116
+ (_client$recordRequest = client.recordRequest) === null || _client$recordRequest === void 0 || _client$recordRequest.call(client, _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({
1117
+ method: details.method,
1118
+ url: details.url,
1119
+ status_code: details.statusCode,
1120
+ duration_ms: details.durationMs,
1121
+ ok,
1122
+ category: details.category
1123
+ }, details.startedAt !== void 0 ? { started_at: new Date(details.startedAt).toISOString() } : {}), details.requestId ? { request_id: details.requestId } : {}), details.responseSize !== void 0 ? { response_size: details.responseSize } : {}), details.requestBody !== void 0 ? { request_body: requestBody.value } : {}), requestBodyTruncated ? { request_body_truncated: true } : {}), details.responseBody !== void 0 ? { response_body: responseBody.value } : {}), responseBodyTruncated ? { response_body_truncated: true } : {}), details.error ? { error: details.error } : {}));
1124
+ } catch (_unused2) {}
1125
+ const slow = details.durationMs >= ((_details$slowThreshol = details.slowThresholdMs) !== null && _details$slowThreshol !== void 0 ? _details$slowThreshol : 1e3);
1126
+ if (!ok || slow) try {
1127
+ client.addBreadcrumb({
1128
+ type: "http",
1129
+ category: details.category,
1130
+ level: ok ? "warning" : "error",
1131
+ timestamp: Date.now(),
1132
+ message: `${details.method} ${details.url} -> ${statusLabel}${ok ? ` (${details.durationMs}ms)` : ""}`,
1133
+ data
1134
+ });
1135
+ } catch (_unused3) {}
1136
+ if (ok) return;
1137
+ try {
1138
+ client.captureMessage(`HTTP ${statusLabel} ${details.method} ${details.url}`, "error", {
1139
+ source: "http",
1140
+ tags: {
1141
+ source: "http",
1142
+ http_method: details.method,
1143
+ http_status: details.statusCode
1144
+ },
1145
+ contexts: { http: data }
1146
+ });
1147
+ } catch (_unused4) {}
1148
+ }
1149
+ /**
1150
+ * 失败请求的兼容入口。
1151
+ *
1152
+ * 保留原函数名,避免已接入的适配层和行为测试需要同时改。
1153
+ */
1154
+ function captureHttpFailure(client, details) {
1155
+ captureHttpRequest(client, _objectSpread2(_objectSpread2({}, details), {}, { ok: false }));
1156
+ }
1157
+ /** 成功请求入口;慢请求会自动写 warning 面包屑。 */
1158
+ function captureHttpSuccess(client, details) {
1159
+ captureHttpRequest(client, _objectSpread2(_objectSpread2({}, details), {}, { ok: true }));
1160
+ }
1161
+ // src/http-capture.ts
1162
+ /** 默认需要脱敏的字段名,匹配时会忽略大小写、下划线和连字符。 */
1163
+ const DEFAULT_HTTP_REDACT_KEYS = [
1164
+ "password",
1165
+ "passwd",
1166
+ "pwd",
1167
+ "token",
1168
+ "accesstoken",
1169
+ "refreshtoken",
1170
+ "idtoken",
1171
+ "authtoken",
1172
+ "jwt",
1173
+ "authorization",
1174
+ "cookie",
1175
+ "setcookie",
1176
+ "secret",
1177
+ "clientsecret",
1178
+ "apikey",
1179
+ "session",
1180
+ "sessionid",
1181
+ "csrf",
1182
+ "creditcard",
1183
+ "cardnumber",
1184
+ "cvv",
1185
+ "idcard",
1186
+ "phone",
1187
+ "mobile",
1188
+ "email"
1189
+ ];
1190
+ /** 字段名归一化,便于匹配 accessToken / access_token / access-token。 */
1191
+ function normalizeHttpRedactKey(key) {
1192
+ return key.toLowerCase().replace(/[_\-\s]/g, "");
1193
+ }
1194
+ /** 判断一个字段名是否在脱敏清单里。 */
1195
+ function isHttpRedactKey(key, redactKeys) {
1196
+ return redactKeys.has(normalizeHttpRedactKey(key));
1197
+ }
1198
+ /** 递归脱敏,保留字段名,只替换值。 */
1199
+ function redactValue(value, redactKeys, depth) {
1200
+ if (depth > 6) return "[MAX_DEPTH]";
1201
+ if (Array.isArray(value)) return value.map((item) => redactValue(item, redactKeys, depth + 1));
1202
+ if (!value || typeof value !== "object") return value;
1203
+ if (Object.prototype.toString.call(value) !== "[object Object]") return value;
1204
+ const result = {};
1205
+ for (const [key, item] of Object.entries(value)) result[key] = isHttpRedactKey(key, redactKeys) ? "[REDACTED]" : redactValue(item, redactKeys, depth + 1);
1206
+ return result;
1207
+ }
1208
+ /** 判断 Content-Type 是否是 JSON。 */
1209
+ function isJsonContentType(contentType) {
1210
+ const value = contentType.toLowerCase();
1211
+ return value.includes("application/json") || value.includes("+json");
1212
+ }
1213
+ /** 安全解析 JSON。 */
1214
+ function parseJson(value) {
1215
+ try {
1216
+ return JSON.parse(value);
1217
+ } catch (_unused) {
1218
+ return;
1219
+ }
1220
+ }
1221
+ /** 解码 form-urlencoded 的 key/value;异常数据保留原值。 */
1222
+ function decodeFormComponent(value) {
1223
+ try {
1224
+ return decodeURIComponent(value.replace(/\+/g, " "));
1225
+ } catch (_unused2) {
1226
+ return value;
1227
+ }
1228
+ }
1229
+ /**
1230
+ * 手动解析 application/x-www-form-urlencoded。
1231
+ *
1232
+ * 不依赖 URLSearchParams:部分小程序基础库和老 WebView 没有这个全局类。
1233
+ */
1234
+ function parseFormUrlEncoded(value) {
1235
+ const result = {};
1236
+ for (const pair of value.split("&")) {
1237
+ if (!pair) continue;
1238
+ const index = pair.indexOf("=");
1239
+ const rawKey = index === -1 ? pair : pair.slice(0, index);
1240
+ const rawValue = index === -1 ? "" : pair.slice(index + 1);
1241
+ result[decodeFormComponent(rawKey)] = decodeFormComponent(rawValue);
1242
+ }
1243
+ return result;
1244
+ }
1245
+ /** 把 fetch / XHR / uni.request / Taro.request 的 body 统一成可序列化对象。 */
1246
+ function normalizeBody(body, contentType) {
1247
+ if (body === null || body === void 0) return void 0;
1248
+ if (typeof body === "string") {
1249
+ const trimmed = body.trim();
1250
+ if (!trimmed) return void 0;
1251
+ if (isJsonContentType(contentType) || /^[[{]/.test(trimmed)) return parseJson(trimmed);
1252
+ if (contentType.toLowerCase().includes("application/x-www-form-urlencoded")) return parseFormUrlEncoded(body);
1253
+ return;
1254
+ }
1255
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
1256
+ const result = {};
1257
+ body.forEach((value, key) => {
1258
+ result[key] = value;
1259
+ });
1260
+ return result;
1261
+ }
1262
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
1263
+ const result = {};
1264
+ body.forEach((value, key) => {
1265
+ result[key] = typeof value === "string" ? value : {
1266
+ file: value.name,
1267
+ size: value.size,
1268
+ type: value.type
1269
+ };
1270
+ });
1271
+ return result;
1272
+ }
1273
+ if (typeof Blob !== "undefined" && body instanceof Blob) return {
1274
+ type: body.type,
1275
+ size: body.size
1276
+ };
1277
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) return {
1278
+ type: "ArrayBuffer",
1279
+ byteLength: body.byteLength
1280
+ };
1281
+ if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body)) return {
1282
+ type: body.constructor.name,
1283
+ byteLength: body.byteLength
1284
+ };
1285
+ if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) return;
1286
+ if (typeof body === "object") return body;
1287
+ }
1288
+ /**
1289
+ * 序列化并脱敏请求体。
1290
+ *
1291
+ * 只处理 JSON、form-urlencoded、FormData 和普通对象;二进制和流式 body
1292
+ * 只记录类型/大小,避免读取原始内容。
1293
+ */
1294
+ function serializeHttpBody(body, contentType, options = {}) {
1295
+ var _options$redactKeys;
1296
+ const normalized = normalizeBody(body, contentType !== null && contentType !== void 0 ? contentType : "");
1297
+ if (normalized === void 0) return void 0;
1298
+ const redacted = redactValue(normalized, new Set([...DEFAULT_HTTP_REDACT_KEYS, ...(_options$redactKeys = options.redactKeys) !== null && _options$redactKeys !== void 0 ? _options$redactKeys : []].map(normalizeHttpRedactKey)), 0);
1299
+ let json;
1300
+ try {
1301
+ json = JSON.stringify(redacted);
1302
+ } catch (_unused3) {
1303
+ return;
1304
+ }
1305
+ if (!json) return void 0;
1306
+ const maxChars = options.maxChars && options.maxChars > 0 ? options.maxChars : 4096;
1307
+ if (json.length <= maxChars) return {
1308
+ value: redacted,
1309
+ truncated: false
1310
+ };
1311
+ return {
1312
+ value: json.slice(0, maxChars),
1313
+ truncated: true
1314
+ };
1315
+ }
1316
+ /** 判断 Content-Type 是否适合按文本保留。 */
1317
+ function isTextContentType(contentType) {
1318
+ const value = contentType.toLowerCase();
1319
+ return value === "" || value.startsWith("text/") || value.includes("json") || value.includes("xml") || value.includes("javascript") || value.includes("x-www-form-urlencoded");
1320
+ }
1321
+ /**
1322
+ * 序列化响应体。
1323
+ *
1324
+ * 与请求体不同,响应体经常是 HTML 错误页或纯文本提示,
1325
+ * JSON 解析失败时保留一段可读文本,避免接口 500 时只看到状态码。
1326
+ * 二进制响应(图片、文件流)直接跳过。
1327
+ */
1328
+ function serializeHttpResponseBody(body, contentType, options = {}) {
1329
+ const capture = serializeHttpBody(body, contentType, options);
1330
+ if (capture) return capture;
1331
+ if (typeof body !== "string") return void 0;
1332
+ if (!isTextContentType(contentType !== null && contentType !== void 0 ? contentType : "")) return void 0;
1333
+ const text = body.trim();
1334
+ if (!text) return void 0;
1335
+ const maxChars = options.maxChars && options.maxChars > 0 ? options.maxChars : 4096;
1336
+ return text.length <= maxChars ? {
1337
+ value: text,
1338
+ truncated: false
1339
+ } : {
1340
+ value: text.slice(0, maxChars),
1341
+ truncated: true
1342
+ };
1343
+ }
1344
+ /** 判断一个 URL 是否是 SDK 自己的上报地址,避免递归上报。 */
1345
+ function isIngestRequestUrl(url) {
1346
+ try {
1347
+ return new URL(url, "http://localhost").pathname.includes("/api/ingest/");
1348
+ } catch (_unused4) {
1349
+ return url.includes("/api/ingest/");
1350
+ }
1351
+ }
1352
+ // src/browser-hooks.ts
1353
+ const MAX_ARG_CHARS = 120;
1354
+ const MAX_MESSAGE_CHARS = 300;
1355
+ const MAX_ARGS = 5;
1356
+ function formatArg(value) {
1357
+ if (typeof value === "string") return value.length <= MAX_ARG_CHARS ? value : `${value.slice(0, MAX_ARG_CHARS)}…`;
1358
+ if (value instanceof Error) return `${value.name}: ${value.message}`.slice(0, MAX_ARG_CHARS);
1359
+ if (value === null) return "null";
1360
+ if (value === void 0) return "undefined";
1361
+ if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
1362
+ if (typeof value === "bigint" || typeof value === "symbol") return String(value);
1363
+ if (typeof value === "object") try {
1364
+ const json = JSON.stringify(value);
1365
+ if (!json) return String(value);
1366
+ return json.length <= MAX_ARG_CHARS ? json : `${json.slice(0, MAX_ARG_CHARS)}…`;
1367
+ } catch (_unused) {
1368
+ return Object.prototype.toString.call(value);
1369
+ }
1370
+ return String(value).slice(0, MAX_ARG_CHARS);
1371
+ }
1372
+ function formatArgs(args) {
1373
+ const text = args.slice(0, MAX_ARGS).map(formatArg).filter((item) => item.length > 0).join(" ");
1374
+ return text.length <= MAX_MESSAGE_CHARS ? text : `${text.slice(0, MAX_MESSAGE_CHARS)}…`;
1375
+ }
1376
+ /** 采集 console.error / console.warn。 */
1377
+ function installConsoleBreadcrumbs(client, options = {}) {
1378
+ if (options.captureConsole === false) return () => {};
1379
+ if (typeof console === "undefined") return () => {};
1380
+ const originalError = console.error;
1381
+ const originalWarn = console.warn;
1382
+ const wrap = (level) => {
1383
+ const original = level === "error" ? originalError : originalWarn;
1384
+ return function(...args) {
1385
+ try {
1386
+ const text = formatArgs(args);
1387
+ client.addBreadcrumb({
1388
+ type: "console",
1389
+ category: level,
1390
+ level: level === "error" ? "error" : "warning",
1391
+ timestamp: Date.now(),
1392
+ message: text ? `console.${level}: ${text}` : `console.${level}`,
1393
+ data: {
1394
+ logger: level,
1395
+ args: args.slice(0, MAX_ARGS).map(formatArg)
1396
+ }
1397
+ });
1398
+ } catch (_unused2) {}
1399
+ original.apply(this, args);
1400
+ };
1401
+ };
1402
+ const wrappedError = wrap("error");
1403
+ const wrappedWarn = wrap("warn");
1404
+ try {
1405
+ console.error = wrappedError;
1406
+ console.warn = wrappedWarn;
1407
+ } catch (_unused3) {
1408
+ return () => {};
1409
+ }
1410
+ return () => {
1411
+ if (console.error === wrappedError) console.error = originalError;
1412
+ if (console.warn === wrappedWarn) console.warn = originalWarn;
1413
+ };
1414
+ }
1415
+ const INTERACTIVE_SELECTOR = "button, a, [role=\"button\"], input, select, textarea, label, [data-testid], [data-test]";
1416
+ function escapeIdentifier(value) {
1417
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
1418
+ return value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`);
1419
+ }
1420
+ function selectorOf(element) {
1421
+ var _element$getAttribute;
1422
+ const testId = (_element$getAttribute = element.getAttribute("data-testid")) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : element.getAttribute("data-test");
1423
+ if (testId) return `[data-testid="${testId.replace(/"/g, "\\\"")}"]`;
1424
+ if (element.id) return `#${escapeIdentifier(element.id)}`;
1425
+ const tag = element.tagName.toLowerCase();
1426
+ const classes = Array.from(element.classList).slice(0, 2);
1427
+ if (classes.length === 0) return tag;
1428
+ return `${tag}.${classes.map(escapeIdentifier).join(".")}`;
1429
+ }
1430
+ function textOf(element) {
1431
+ var _ref, _ref2, _ref3, _element$getAttribute2;
1432
+ const raw = ((_ref = (_ref2 = (_ref3 = (_element$getAttribute2 = element.getAttribute("aria-label")) !== null && _element$getAttribute2 !== void 0 ? _element$getAttribute2 : element.getAttribute("title")) !== null && _ref3 !== void 0 ? _ref3 : element.getAttribute("placeholder")) !== null && _ref2 !== void 0 ? _ref2 : element.textContent) !== null && _ref !== void 0 ? _ref : "").replace(/\s+/g, " ").trim();
1433
+ if (!raw) return void 0;
1434
+ return raw.length > 60 ? `${raw.slice(0, 60)}…` : raw;
1435
+ }
1436
+ function describe(element) {
1437
+ const info = {
1438
+ selector: selectorOf(element),
1439
+ tag: element.tagName.toLowerCase()
1440
+ };
1441
+ const text = textOf(element);
1442
+ if (text) info.text = text;
1443
+ if (element.id) info.id = element.id;
1444
+ if (element.classList.length > 0) info.classes = Array.from(element.classList).slice(0, 4);
1445
+ for (const [key, attribute] of [
1446
+ ["role", "role"],
1447
+ ["aria_label", "aria-label"],
1448
+ ["name", "name"],
1449
+ ["type", "type"]
1450
+ ]) {
1451
+ const value = element.getAttribute(attribute);
1452
+ if (value) info[key] = value;
1453
+ }
1454
+ if (element.tagName === "A") {
1455
+ const href = element.getAttribute("href");
1456
+ if (href) info.href = href;
1457
+ }
1458
+ return info;
1459
+ }
1460
+ /** 记录点击面包屑;没有 DOM 的小程序自动 no-op。 */
1461
+ function installClickBreadcrumbs(client) {
1462
+ if (typeof document === "undefined") return () => {};
1463
+ const handler = (event) => {
1464
+ const target = event.target;
1465
+ if (!(target instanceof Element)) return;
1466
+ if (target === document.documentElement || target === document.body) return;
1467
+ try {
1468
+ var _target$closest;
1469
+ const info = describe((_target$closest = target.closest(INTERACTIVE_SELECTOR)) !== null && _target$closest !== void 0 ? _target$closest : target);
1470
+ client.addBreadcrumb({
1471
+ type: "ui",
1472
+ category: "click",
1473
+ level: "info",
1474
+ timestamp: Date.now(),
1475
+ message: info.text ? `点击「${info.text}」` : `点击 ${info.selector}`,
1476
+ data: info
1477
+ });
1478
+ } catch (_unused4) {}
1479
+ };
1480
+ document.addEventListener("click", handler, true);
1481
+ return () => document.removeEventListener("click", handler, true);
1482
+ }
1483
+ /** 资源加载失败;没有 window 的小程序自动 no-op。 */
1484
+ function installResourceErrorHooks(client) {
1485
+ if (typeof window === "undefined") return () => {};
1486
+ const handleResourceError = (event) => {
1487
+ try {
1488
+ var _ref4, _target$src;
1489
+ const target = event.target;
1490
+ if (!target || typeof target.tagName !== "string") return;
1491
+ const url = (_ref4 = (_target$src = target.src) !== null && _target$src !== void 0 ? _target$src : target.href) !== null && _ref4 !== void 0 ? _ref4 : "";
1492
+ if (!url) return;
1493
+ client.captureMessage(`资源加载失败: ${url}`, "warning", {
1494
+ source: "resource",
1495
+ tags: { resource: target.tagName.toLowerCase() }
1496
+ });
1497
+ } catch (_unused5) {}
1498
+ };
1499
+ window.addEventListener("error", handleResourceError, true);
1500
+ return () => {
1501
+ window.removeEventListener("error", handleResourceError, true);
1502
+ };
1503
+ }
1504
+ /** 页面可见性与网络状态面包屑;没有 DOM 的小程序自动 no-op。 */
1505
+ function installLifecycleBreadcrumbs(client, options = {}) {
1506
+ if (options.captureLifecycle === false) return () => {};
1507
+ if (typeof window === "undefined" || typeof document === "undefined") return () => {};
1508
+ const add = (category, message, data) => {
1509
+ try {
1510
+ client.addBreadcrumb({
1511
+ type: "lifecycle",
1512
+ category,
1513
+ level: "info",
1514
+ timestamp: Date.now(),
1515
+ message,
1516
+ data
1517
+ });
1518
+ } catch (_unused6) {}
1519
+ };
1520
+ const handleVisibility = () => {
1521
+ var _window$location;
1522
+ add("visibility", `页面${document.visibilityState === "hidden" ? "隐藏" : "可见"}`, {
1523
+ state: document.visibilityState,
1524
+ url: (_window$location = window.location) === null || _window$location === void 0 ? void 0 : _window$location.href
1525
+ });
1526
+ };
1527
+ const handleOnline = () => {
1528
+ var _window$location2;
1529
+ add("network", "网络恢复", {
1530
+ online: true,
1531
+ url: (_window$location2 = window.location) === null || _window$location2 === void 0 ? void 0 : _window$location2.href
1532
+ });
1533
+ };
1534
+ const handleOffline = () => {
1535
+ var _window$location3;
1536
+ add("network", "网络断开", {
1537
+ online: false,
1538
+ url: (_window$location3 = window.location) === null || _window$location3 === void 0 ? void 0 : _window$location3.href
1539
+ });
1540
+ };
1541
+ document.addEventListener("visibilitychange", handleVisibility);
1542
+ window.addEventListener("online", handleOnline);
1543
+ window.addEventListener("offline", handleOffline);
1544
+ return () => {
1545
+ document.removeEventListener("visibilitychange", handleVisibility);
1546
+ window.removeEventListener("online", handleOnline);
1547
+ window.removeEventListener("offline", handleOffline);
1548
+ };
1549
+ }
1550
+ exports.BreadcrumbStore = BreadcrumbStore;
1551
+ exports.DEFAULT_FLUSH_INTERVAL_MS = DEFAULT_FLUSH_INTERVAL_MS;
1552
+ exports.DEFAULT_HTTP_REDACT_KEYS = DEFAULT_HTTP_REDACT_KEYS;
1553
+ exports.DEFAULT_MAX_BREADCRUMBS = DEFAULT_MAX_BREADCRUMBS;
1554
+ exports.DEFAULT_MAX_HTTP_RESPONSE_CHARS = DEFAULT_MAX_HTTP_RESPONSE_CHARS;
1555
+ exports.DEFAULT_MAX_QUEUE_SIZE = DEFAULT_MAX_QUEUE_SIZE;
1556
+ exports.DEFAULT_MAX_REQUEST_LOG = DEFAULT_MAX_REQUEST_LOG;
1557
+ exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES;
1558
+ exports.DEFAULT_RETRY_BASE_DELAY_MS = DEFAULT_RETRY_BASE_DELAY_MS;
1559
+ exports.DEFAULT_RETRY_MAX_DELAY_MS = DEFAULT_RETRY_MAX_DELAY_MS;
1560
+ exports.DEFAULT_SLOW_REQUEST_THRESHOLD_MS = DEFAULT_SLOW_REQUEST_THRESHOLD_MS;
1561
+ exports.ErrorThrottle = ErrorThrottle;
1562
+ exports.EventQueue = EventQueue;
1563
+ exports.FireflyTransportError = FireflyTransportError;
1564
+ exports.PageSessionTracker = PageSessionTracker;
1565
+ exports.RequestLogStore = RequestLogStore;
1566
+ exports.Scope = Scope;
1567
+ exports.captureHttpFailure = captureHttpFailure;
1568
+ exports.captureHttpRequest = captureHttpRequest;
1569
+ exports.captureHttpSuccess = captureHttpSuccess;
1570
+ exports.createClient = createClient;
1571
+ exports.createEventId = createEventId;
1572
+ exports.installClickBreadcrumbs = installClickBreadcrumbs;
1573
+ exports.installConsoleBreadcrumbs = installConsoleBreadcrumbs;
1574
+ exports.installLifecycleBreadcrumbs = installLifecycleBreadcrumbs;
1575
+ exports.installResourceErrorHooks = installResourceErrorHooks;
1576
+ exports.isHttpRedactKey = isHttpRedactKey;
1577
+ exports.isIngestRequestUrl = isIngestRequestUrl;
1578
+ exports.isRetryableStatus = isRetryableStatus;
1579
+ exports.normalizeHttpRedactKey = normalizeHttpRedactKey;
1580
+ exports.parseRetryAfter = parseRetryAfter;
1581
+ exports.resolveOptions = resolveOptions;
1582
+ exports.serializeHttpBody = serializeHttpBody;
1583
+ exports.serializeHttpResponseBody = serializeHttpResponseBody;
1584
+
1585
+ //# sourceMappingURL=index.cjs.map