@botim/mp-debug-sdk 0.1.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.js ADDED
@@ -0,0 +1,1375 @@
1
+ // src/types.ts
2
+ var SCHEMA_VERSION = 2;
3
+
4
+ // src/errors.ts
5
+ var BRAND = "@botim/debug-sdk";
6
+ var BotimConfigError = class extends Error {
7
+ constructor(message, opts = { code: "invalid-config" }) {
8
+ super(`[${BRAND}] ${message}`);
9
+ this.name = "BotimConfigError";
10
+ this.code = opts.code;
11
+ this.path = opts.path;
12
+ }
13
+ };
14
+ var BotimConsentError = class extends Error {
15
+ constructor(message) {
16
+ super(`[${BRAND}] ${message}`);
17
+ this.name = "BotimConsentError";
18
+ }
19
+ };
20
+
21
+ // src/buffer.ts
22
+ var RingBuffer = class {
23
+ constructor(opts) {
24
+ this.opts = opts;
25
+ this.items = [];
26
+ this.dropped = 0;
27
+ }
28
+ push(event) {
29
+ if (this.items.length >= this.opts.capacity) {
30
+ this.items.shift();
31
+ this.dropped++;
32
+ this.opts.onDrop?.(1);
33
+ }
34
+ this.items.push(event);
35
+ }
36
+ drain(max) {
37
+ if (max >= this.items.length) {
38
+ const out = this.items;
39
+ this.items = [];
40
+ return out;
41
+ }
42
+ return this.items.splice(0, max);
43
+ }
44
+ size() {
45
+ return this.items.length;
46
+ }
47
+ droppedCount() {
48
+ return this.dropped;
49
+ }
50
+ };
51
+
52
+ // src/transport.ts
53
+ var DEFAULT_MAX_RETRIES = 3;
54
+ var DEFAULT_POLL_TIMEOUT_MS = 25e3;
55
+ var DEFAULT_MIN_BACKOFF_MS = 500;
56
+ var DEFAULT_MAX_BACKOFF_MS = 3e4;
57
+ var Transport = class {
58
+ constructor(opts) {
59
+ this.opts = opts;
60
+ this.timer = null;
61
+ this.inflightUpload = null;
62
+ this.inflightPoll = null;
63
+ this.commandLoopRunning = false;
64
+ this.stopped = false;
65
+ this.flushing = false;
66
+ this.internalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : ((..._args) => {
67
+ throw new Error("[@botim/debug-sdk] fetch is not available in this environment");
68
+ });
69
+ }
70
+ start() {
71
+ if (this.timer) return;
72
+ this.timer = setInterval(
73
+ () => void this.flush().catch((err) => this.opts.onError?.(err)),
74
+ this.opts.flushIntervalMs
75
+ );
76
+ }
77
+ async flush() {
78
+ while (!this.stopped && this.opts.buffer.size() > 0) {
79
+ await this.flushOnce();
80
+ }
81
+ }
82
+ async flushOnce() {
83
+ if (this.flushing || this.opts.buffer.size() === 0) return;
84
+ this.flushing = true;
85
+ try {
86
+ const events = this.opts.buffer.drain(this.opts.maxBatchSize);
87
+ if (events.length === 0) return;
88
+ const batch = {
89
+ sessionToken: this.opts.deviceToken,
90
+ events
91
+ };
92
+ await this.send(batch, 0);
93
+ } finally {
94
+ this.flushing = false;
95
+ }
96
+ }
97
+ async send(batch, attempt) {
98
+ if (this.stopped) return;
99
+ this.inflightUpload = new AbortController();
100
+ try {
101
+ const res = await this.internalFetch(this.opts.ingestUrl, {
102
+ method: "POST",
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ Authorization: `Bearer ${this.opts.deviceToken}`
106
+ },
107
+ body: JSON.stringify(batch),
108
+ signal: this.inflightUpload.signal,
109
+ keepalive: true
110
+ });
111
+ if (!res.ok) {
112
+ throw new Error(`ingest http ${res.status}`);
113
+ }
114
+ } catch (err) {
115
+ if (this.stopped) return;
116
+ const max = this.opts.maxRetries ?? DEFAULT_MAX_RETRIES;
117
+ if (attempt >= max) {
118
+ this.opts.onError?.(err);
119
+ return;
120
+ }
121
+ const backoff = 250 * Math.pow(4, attempt);
122
+ await new Promise((r) => setTimeout(r, backoff));
123
+ return this.send(batch, attempt + 1);
124
+ } finally {
125
+ this.inflightUpload = null;
126
+ }
127
+ }
128
+ /**
129
+ * Start the AI command long-poll loop. Maintains AT MOST ONE in-flight
130
+ * request to `commandPollUrl` at any time. Errors are reported via
131
+ * `onError` and trigger exponential backoff. Stop with `transport.stop()`.
132
+ */
133
+ startCommandLoop(opts) {
134
+ if (this.commandLoopRunning) return;
135
+ this.commandLoopRunning = true;
136
+ const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
137
+ const minBackoff = opts.minBackoffMs ?? DEFAULT_MIN_BACKOFF_MS;
138
+ const maxBackoff = opts.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
139
+ let attempt = 0;
140
+ const loop = async () => {
141
+ while (!this.stopped) {
142
+ this.inflightPoll = new AbortController();
143
+ const timeoutId = setTimeout(() => {
144
+ this.inflightPoll?.abort();
145
+ }, pollTimeout);
146
+ try {
147
+ const res = await this.internalFetch(opts.commandPollUrl, {
148
+ method: "GET",
149
+ headers: { Authorization: `Bearer ${this.opts.deviceToken}` },
150
+ signal: this.inflightPoll.signal
151
+ });
152
+ if (!res.ok) {
153
+ throw new Error(`command-poll http ${res.status}`);
154
+ }
155
+ if (res.status === 204) {
156
+ await sleep(minBackoff);
157
+ attempt = 0;
158
+ continue;
159
+ }
160
+ const json = await res.json();
161
+ attempt = 0;
162
+ for (const cmd of json.commands ?? []) {
163
+ try {
164
+ await opts.onCommand(cmd);
165
+ } catch (err) {
166
+ this.opts.onError?.(err);
167
+ }
168
+ }
169
+ if (typeof json.nextDelayMs === "number" && json.nextDelayMs > 0) {
170
+ await sleep(Math.min(json.nextDelayMs, maxBackoff));
171
+ }
172
+ } catch (err) {
173
+ if (this.stopped) return;
174
+ const aborted = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
175
+ if (!aborted) {
176
+ this.opts.onError?.(err);
177
+ attempt += 1;
178
+ } else {
179
+ attempt = 0;
180
+ }
181
+ const backoff = Math.min(
182
+ maxBackoff,
183
+ minBackoff * Math.pow(2, Math.max(0, attempt - 1))
184
+ );
185
+ await sleep(backoff);
186
+ } finally {
187
+ clearTimeout(timeoutId);
188
+ this.inflightPoll = null;
189
+ }
190
+ }
191
+ };
192
+ void loop();
193
+ }
194
+ async stop() {
195
+ if (this.timer) {
196
+ clearInterval(this.timer);
197
+ this.timer = null;
198
+ }
199
+ if (this.opts.buffer.size() > 0) {
200
+ const events = this.opts.buffer.drain(this.opts.buffer.size());
201
+ try {
202
+ await this.internalFetch(this.opts.ingestUrl, {
203
+ method: "POST",
204
+ headers: {
205
+ "Content-Type": "application/json",
206
+ Authorization: `Bearer ${this.opts.deviceToken}`
207
+ },
208
+ body: JSON.stringify({
209
+ sessionToken: this.opts.deviceToken,
210
+ events
211
+ }),
212
+ keepalive: true
213
+ });
214
+ } catch (err) {
215
+ this.opts.onError?.(err);
216
+ }
217
+ }
218
+ this.stopped = true;
219
+ this.commandLoopRunning = false;
220
+ this.inflightUpload?.abort();
221
+ this.inflightPoll?.abort();
222
+ }
223
+ };
224
+ function sleep(ms) {
225
+ return new Promise((r) => setTimeout(r, ms));
226
+ }
227
+
228
+ // src/attach.ts
229
+ async function attachDevice(endpoint, config, deviceInfo, consent) {
230
+ const base = endpoint.replace(/\/$/, "");
231
+ const url = `${base}/v1/attach`;
232
+ const body = {
233
+ miniProgramId: config.miniProgramId,
234
+ env: config.env,
235
+ buildSignature: config.buildSignature,
236
+ deviceInfo,
237
+ consent,
238
+ schemaVersion: SCHEMA_VERSION
239
+ };
240
+ const res = await fetch(url, {
241
+ method: "POST",
242
+ headers: { "Content-Type": "application/json" },
243
+ body: JSON.stringify(body)
244
+ });
245
+ if (!res.ok) {
246
+ const text = await res.text().catch(() => "");
247
+ throw new Error(
248
+ `[@botim/debug-sdk] attach failed: ${res.status} ${res.statusText} ${text}`.trim()
249
+ );
250
+ }
251
+ const json = await res.json();
252
+ if (!json?.sid || !json?.deviceToken || !json?.ingestUrl || !json?.commandPollUrl) {
253
+ throw new Error("[@botim/debug-sdk] attach response missing required fields");
254
+ }
255
+ return {
256
+ ...json,
257
+ ingestUrl: resolveAgainstEndpoint(json.ingestUrl, base),
258
+ commandPollUrl: resolveAgainstEndpoint(json.commandPollUrl, base)
259
+ };
260
+ }
261
+ function resolveAgainstEndpoint(url, base) {
262
+ if (/^https?:\/\//i.test(url)) return url;
263
+ const path = url.startsWith("/") ? url : "/" + url;
264
+ return base + path;
265
+ }
266
+ function detectDeviceInfo(app, override) {
267
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : void 0;
268
+ return {
269
+ deviceId: override?.deviceId ?? generateDeviceId(),
270
+ platform: override?.platform ?? detectPlatform(ua),
271
+ osVersion: override?.osVersion,
272
+ appName: override?.appName ?? app.name,
273
+ appVersion: override?.appVersion ?? app.version,
274
+ appBuild: override?.appBuild ?? app.build,
275
+ userAgent: override?.userAgent ?? ua
276
+ };
277
+ }
278
+ function detectPlatform(ua) {
279
+ if (!ua) return "unknown";
280
+ if (/Android/i.test(ua)) return "android";
281
+ if (/iPhone|iPad|iPod/i.test(ua)) return "ios";
282
+ if (/Mozilla|Chrome|Safari|Firefox/i.test(ua)) return "web";
283
+ return "unknown";
284
+ }
285
+ function generateDeviceId() {
286
+ const c = typeof crypto !== "undefined" ? crypto : void 0;
287
+ if (c?.randomUUID) return c.randomUUID();
288
+ return "dev-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
289
+ }
290
+
291
+ // src/redact.ts
292
+ var REDACTED = "[REDACTED]";
293
+ function redactHeaders(headers, redactList) {
294
+ if (!headers) return headers;
295
+ const lower = new Set(redactList.map((h) => h.toLowerCase()));
296
+ const out = {};
297
+ for (const [k, v] of Object.entries(headers)) {
298
+ out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
299
+ }
300
+ return out;
301
+ }
302
+ function redactBody(body, patterns, maxBytes) {
303
+ if (body === void 0) return { body: void 0, truncated: false };
304
+ let out = body;
305
+ for (const pat of patterns) {
306
+ if (pat.global) pat.lastIndex = 0;
307
+ out = out.replace(pat, REDACTED);
308
+ }
309
+ let truncated = false;
310
+ if (out.length > maxBytes) {
311
+ out = out.slice(0, maxBytes);
312
+ truncated = true;
313
+ }
314
+ return { body: out, truncated };
315
+ }
316
+ function headersFromInit(init) {
317
+ if (!init) return void 0;
318
+ const out = {};
319
+ if (init instanceof Headers) {
320
+ init.forEach((v, k) => out[k] = v);
321
+ } else if (Array.isArray(init)) {
322
+ for (const [k, v] of init) out[k] = v;
323
+ } else {
324
+ Object.assign(out, init);
325
+ }
326
+ return out;
327
+ }
328
+ function headersFromResponse(res) {
329
+ const out = {};
330
+ res.headers.forEach((v, k) => out[k] = v);
331
+ return out;
332
+ }
333
+ async function readBodyForCapture(body) {
334
+ if (body == null) return void 0;
335
+ if (typeof body === "string") return body;
336
+ if (body instanceof URLSearchParams) return body.toString();
337
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
338
+ try {
339
+ return new TextDecoder().decode(body);
340
+ } catch {
341
+ return "[binary]";
342
+ }
343
+ }
344
+ if (body instanceof Blob) {
345
+ try {
346
+ return await body.text();
347
+ } catch {
348
+ return "[blob]";
349
+ }
350
+ }
351
+ if (body instanceof FormData) {
352
+ const parts = [];
353
+ body.forEach((v, k) => parts.push(`${k}=${typeof v === "string" ? v : "[file]"}`));
354
+ return parts.join("&");
355
+ }
356
+ return void 0;
357
+ }
358
+
359
+ // src/serialize.ts
360
+ var MAX_STRING_LEN = 2048;
361
+ var MAX_OBJECT_BYTES = 4096;
362
+ function serializeArg(value) {
363
+ if (value === void 0) return { k: "undefined" };
364
+ if (value === null) return { k: "primitive", v: null };
365
+ const t = typeof value;
366
+ if (t === "boolean" || t === "number") {
367
+ return { k: "primitive", v: value };
368
+ }
369
+ if (t === "bigint") {
370
+ return { k: "primitive", v: value.toString() + "n" };
371
+ }
372
+ if (t === "string") {
373
+ const s = value;
374
+ if (s.length > MAX_STRING_LEN) {
375
+ return { k: "string", v: s.slice(0, MAX_STRING_LEN), truncated: true };
376
+ }
377
+ return { k: "string", v: s };
378
+ }
379
+ if (t === "function") {
380
+ return { k: "function", name: value.name || void 0 };
381
+ }
382
+ if (value instanceof Error) {
383
+ return { k: "error", name: value.name, message: value.message, stack: value.stack };
384
+ }
385
+ try {
386
+ const seen = /* @__PURE__ */ new WeakSet();
387
+ const json = JSON.stringify(value, function replacer(_k, v) {
388
+ if (typeof v === "bigint") return v.toString() + "n";
389
+ if (typeof v === "function") return `[Function: ${v.name || "anonymous"}]`;
390
+ if (typeof v === "object" && v !== null) {
391
+ if (seen.has(v)) return "[Circular]";
392
+ seen.add(v);
393
+ }
394
+ return v;
395
+ });
396
+ if (json === void 0) {
397
+ return { k: "unserializable", reason: "undefined-result" };
398
+ }
399
+ if (json.length > MAX_OBJECT_BYTES) {
400
+ return { k: "json", v: json.slice(0, MAX_OBJECT_BYTES), truncated: true };
401
+ }
402
+ return { k: "json", v: json };
403
+ } catch (err) {
404
+ return {
405
+ k: "unserializable",
406
+ reason: err instanceof Error ? err.message : "unknown"
407
+ };
408
+ }
409
+ }
410
+
411
+ // src/interceptors/console.ts
412
+ var METHODS = ["log", "info", "warn", "error", "debug"];
413
+ var LEVEL = {
414
+ log: "info",
415
+ info: "info",
416
+ warn: "warn",
417
+ error: "error",
418
+ debug: "debug"
419
+ };
420
+ function installConsoleInterceptor(opts) {
421
+ const originals = {};
422
+ let active = false;
423
+ for (const m of METHODS) {
424
+ const original = console[m];
425
+ originals[m] = original;
426
+ console[m] = function patched(...args) {
427
+ original.apply(console, args);
428
+ if (active) return;
429
+ if (opts.sample < 1 && Math.random() > opts.sample) return;
430
+ active = true;
431
+ try {
432
+ const payload = {
433
+ method: m,
434
+ args: args.map(serializeArg)
435
+ };
436
+ opts.emit(LEVEL[m], payload);
437
+ } catch {
438
+ } finally {
439
+ active = false;
440
+ }
441
+ };
442
+ }
443
+ return () => {
444
+ for (const m of METHODS) {
445
+ const original = originals[m];
446
+ if (original) {
447
+ console[m] = original;
448
+ }
449
+ }
450
+ };
451
+ }
452
+
453
+ // src/interceptors/errors.ts
454
+ function installErrorInterceptor(opts) {
455
+ const target = resolveTarget();
456
+ if (!target) return () => {
457
+ };
458
+ const onError = (ev) => {
459
+ const e = ev;
460
+ const err = e.error;
461
+ opts.emit({
462
+ message: e.message || (err instanceof Error ? err.message : "unknown error"),
463
+ name: err instanceof Error ? err.name : void 0,
464
+ stack: err instanceof Error ? err.stack : void 0,
465
+ source: "window.error",
466
+ filename: e.filename,
467
+ lineno: e.lineno,
468
+ colno: e.colno
469
+ });
470
+ };
471
+ const onRejection = (ev) => {
472
+ const e = ev;
473
+ const reason = e.reason;
474
+ opts.emit({
475
+ message: reason instanceof Error ? reason.message : safeString(reason),
476
+ name: reason instanceof Error ? reason.name : void 0,
477
+ stack: reason instanceof Error ? reason.stack : void 0,
478
+ source: "unhandledrejection"
479
+ });
480
+ };
481
+ target.addEventListener("error", onError);
482
+ target.addEventListener("unhandledrejection", onRejection);
483
+ return () => {
484
+ target.removeEventListener("error", onError);
485
+ target.removeEventListener("unhandledrejection", onRejection);
486
+ };
487
+ }
488
+ function resolveTarget() {
489
+ const w = typeof window !== "undefined" ? window : globalThis;
490
+ if (w && typeof w.addEventListener === "function" && typeof w.removeEventListener === "function") {
491
+ return w;
492
+ }
493
+ return void 0;
494
+ }
495
+ function safeString(v) {
496
+ try {
497
+ return typeof v === "string" ? v : JSON.stringify(v) ?? String(v);
498
+ } catch {
499
+ return String(v);
500
+ }
501
+ }
502
+
503
+ // src/interceptors/network.ts
504
+ function installNetworkInterceptor(opts) {
505
+ const restoreFetch = wrapFetch(opts);
506
+ const restoreXHR = wrapXHR(opts);
507
+ return () => {
508
+ restoreFetch();
509
+ restoreXHR();
510
+ };
511
+ }
512
+ function shouldSample(rate) {
513
+ return rate >= 1 || Math.random() < rate;
514
+ }
515
+ function newReqId() {
516
+ return "r_" + Math.random().toString(36).slice(2, 10);
517
+ }
518
+ function wrapFetch(opts) {
519
+ if (typeof fetch !== "function") return () => {
520
+ };
521
+ const original = fetch;
522
+ const target = globalThis;
523
+ target.fetch = async function patchedFetch(input, init) {
524
+ if (!shouldSample(opts.sample)) return original.call(this, input, init);
525
+ const reqId = newReqId();
526
+ const start = Date.now();
527
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
528
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
529
+ if (opts.shouldSkip && opts.shouldSkip(url, method)) {
530
+ return original.call(this, input, init);
531
+ }
532
+ let reqHeaders;
533
+ let reqBody;
534
+ try {
535
+ if (input instanceof Request) {
536
+ reqHeaders = headersFromResponse(input.clone());
537
+ reqBody = await input.clone().text().catch(() => void 0);
538
+ } else {
539
+ reqHeaders = headersFromInit(init?.headers);
540
+ reqBody = await readBodyForCapture(init?.body);
541
+ }
542
+ } catch {
543
+ }
544
+ opts.emit({ phase: "request", reqId, method, url, reqHeaders, reqBody });
545
+ try {
546
+ const res = await original.call(this, input, init);
547
+ let resBody;
548
+ try {
549
+ resBody = await res.clone().text();
550
+ } catch {
551
+ resBody = void 0;
552
+ }
553
+ opts.emit({
554
+ phase: "response",
555
+ reqId,
556
+ method,
557
+ url,
558
+ status: res.status,
559
+ durationMs: Date.now() - start,
560
+ resHeaders: headersFromResponse(res),
561
+ resBody
562
+ });
563
+ return res;
564
+ } catch (err) {
565
+ opts.emit({
566
+ phase: "error",
567
+ reqId,
568
+ method,
569
+ url,
570
+ durationMs: Date.now() - start,
571
+ errorMessage: err instanceof Error ? err.message : String(err),
572
+ errorName: err instanceof Error ? err.name : void 0
573
+ });
574
+ throw err;
575
+ }
576
+ };
577
+ return () => {
578
+ target.fetch = original;
579
+ };
580
+ }
581
+ function wrapXHR(opts) {
582
+ if (typeof XMLHttpRequest === "undefined") return () => {
583
+ };
584
+ const proto = XMLHttpRequest.prototype;
585
+ const states = /* @__PURE__ */ new WeakMap();
586
+ const origOpen = proto.open;
587
+ const origSend = proto.send;
588
+ const origSetReqHeader = proto.setRequestHeader;
589
+ proto.open = function patchedOpen(method, url, ...rest) {
590
+ const resolvedUrl = typeof url === "string" ? url : url.toString();
591
+ const upperMethod = method.toUpperCase();
592
+ const skipped = opts.shouldSkip ? opts.shouldSkip(resolvedUrl, upperMethod) : false;
593
+ const state = {
594
+ reqId: newReqId(),
595
+ start: 0,
596
+ method: upperMethod,
597
+ url: resolvedUrl,
598
+ reqHeaders: {},
599
+ sampled: !skipped && shouldSample(opts.sample)
600
+ };
601
+ states.set(this, state);
602
+ return origOpen.apply(this, [method, url, ...rest]);
603
+ };
604
+ proto.setRequestHeader = function patchedSetHeader(name, value) {
605
+ const s = states.get(this);
606
+ if (s) s.reqHeaders[name] = value;
607
+ return origSetReqHeader.call(this, name, value);
608
+ };
609
+ proto.send = function patchedSend(body) {
610
+ const s = states.get(this);
611
+ if (!s || !s.sampled) {
612
+ return origSend.apply(this, [body]);
613
+ }
614
+ s.start = Date.now();
615
+ s.reqBody = typeof body === "string" ? body : void 0;
616
+ opts.emit({
617
+ phase: "request",
618
+ reqId: s.reqId,
619
+ method: s.method,
620
+ url: s.url,
621
+ reqHeaders: s.reqHeaders,
622
+ reqBody: s.reqBody
623
+ });
624
+ const onLoad = () => {
625
+ const headers = parseXhrHeaders(this.getAllResponseHeaders());
626
+ const resBody = typeof this.response === "string" ? this.response : void 0;
627
+ opts.emit({
628
+ phase: "response",
629
+ reqId: s.reqId,
630
+ method: s.method,
631
+ url: s.url,
632
+ status: this.status,
633
+ durationMs: Date.now() - s.start,
634
+ resHeaders: headers,
635
+ resBody
636
+ });
637
+ };
638
+ const onError = () => {
639
+ opts.emit({
640
+ phase: "error",
641
+ reqId: s.reqId,
642
+ method: s.method,
643
+ url: s.url,
644
+ durationMs: Date.now() - s.start,
645
+ errorMessage: this.statusText || "xhr error"
646
+ });
647
+ };
648
+ this.addEventListener("load", onLoad);
649
+ this.addEventListener("error", onError);
650
+ this.addEventListener("timeout", onError);
651
+ this.addEventListener("abort", onError);
652
+ return origSend.apply(this, [body]);
653
+ };
654
+ return () => {
655
+ proto.open = origOpen;
656
+ proto.send = origSend;
657
+ proto.setRequestHeader = origSetReqHeader;
658
+ };
659
+ }
660
+ function parseXhrHeaders(raw) {
661
+ const out = {};
662
+ if (!raw) return out;
663
+ for (const line of raw.split(/\r?\n/)) {
664
+ const idx = line.indexOf(":");
665
+ if (idx < 0) continue;
666
+ const k = line.slice(0, idx).trim();
667
+ const v = line.slice(idx + 1).trim();
668
+ if (k) out[k] = v;
669
+ }
670
+ return out;
671
+ }
672
+
673
+ // src/commands/registry.ts
674
+ var NAME_PATTERN = /^[a-z][a-z0-9-]{1,47}$/;
675
+ var CommandRegistry = class {
676
+ constructor() {
677
+ this.handlers = /* @__PURE__ */ new Map();
678
+ this.overrideListeners = [];
679
+ }
680
+ register(name, handler) {
681
+ if (typeof name !== "string" || !NAME_PATTERN.test(name)) {
682
+ throw new Error(
683
+ `[@botim/debug-sdk] invalid command name ${JSON.stringify(name)}; expected kebab-case matching ${NAME_PATTERN}`
684
+ );
685
+ }
686
+ if (typeof handler !== "function") {
687
+ throw new Error(`[@botim/debug-sdk] command "${name}" handler must be a function`);
688
+ }
689
+ const isOverride = this.handlers.has(name);
690
+ this.handlers.set(name, handler);
691
+ if (isOverride) {
692
+ for (const fn of this.overrideListeners) {
693
+ try {
694
+ fn(name);
695
+ } catch {
696
+ }
697
+ }
698
+ }
699
+ }
700
+ unregister(name) {
701
+ return this.handlers.delete(name);
702
+ }
703
+ has(name) {
704
+ return this.handlers.has(name);
705
+ }
706
+ /** Subscribe to override events; returns an unsubscribe fn. */
707
+ onOverride(fn) {
708
+ this.overrideListeners.push(fn);
709
+ return () => {
710
+ this.overrideListeners = this.overrideListeners.filter((f) => f !== fn);
711
+ };
712
+ }
713
+ /**
714
+ * Dispatch a single command. Never throws; returns a typed result.
715
+ * Caller is responsible for mapping the result onto an event envelope.
716
+ */
717
+ async dispatch(req, signal) {
718
+ const handler = this.handlers.get(req.name);
719
+ if (!handler) {
720
+ return {
721
+ kind: "rejected",
722
+ payload: { command: req.name, reason: "unknown-command" }
723
+ };
724
+ }
725
+ const start = Date.now();
726
+ try {
727
+ const args = req.args ?? {};
728
+ const result = await Promise.resolve(
729
+ handler(args, { commandId: req.id, command: req.name, signal })
730
+ );
731
+ return {
732
+ kind: "ack",
733
+ payload: {
734
+ command: req.name,
735
+ ok: true,
736
+ result,
737
+ durationMs: Date.now() - start
738
+ }
739
+ };
740
+ } catch (err) {
741
+ return {
742
+ kind: "rejected",
743
+ payload: {
744
+ command: req.name,
745
+ reason: "handler-threw",
746
+ details: err instanceof Error ? err.message : String(err)
747
+ }
748
+ };
749
+ }
750
+ }
751
+ };
752
+
753
+ // src/commands/builtins.ts
754
+ var MAX_DUMP_BYTES = 64 * 1024;
755
+ var MAX_SCREENSHOT_BYTES = 1024 * 1024;
756
+ async function defaultDomScreenshot() {
757
+ if (typeof document === "undefined" || typeof window === "undefined") {
758
+ throw new Error(
759
+ "[@botim/debug-sdk] default screenshot requires a DOM. Provide builtins.screenshot for non-browser runtimes (e.g. native bridge)."
760
+ );
761
+ }
762
+ try {
763
+ return await captureViaSvgForeignObject();
764
+ } catch (err) {
765
+ try {
766
+ console.warn(
767
+ "[@botim/debug-sdk] SVG/canvas screenshot failed, falling back to HTML snapshot:",
768
+ err instanceof Error ? err.message : err
769
+ );
770
+ } catch {
771
+ }
772
+ return await captureViaHtmlSnapshot();
773
+ }
774
+ }
775
+ async function captureViaSvgForeignObject() {
776
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
777
+ const w = Math.max(1, Math.min(window.innerWidth || document.documentElement.clientWidth || 1024, 2400));
778
+ const h = Math.max(1, Math.min(document.documentElement.scrollHeight, 4e3));
779
+ const inlinedCss = readInlineableStyles();
780
+ const clone = document.documentElement.cloneNode(true);
781
+ clone.querySelectorAll('link[rel="stylesheet"]').forEach((el) => el.remove());
782
+ clone.querySelectorAll("script,noscript,template").forEach((el) => el.remove());
783
+ clone.querySelectorAll("canvas").forEach((el) => el.remove());
784
+ clone.querySelectorAll("img").forEach((img) => {
785
+ const src = img.getAttribute("src") || "";
786
+ if (src && /^(https?:|\/\/)/i.test(src)) {
787
+ try {
788
+ const u = new URL(src, location.href);
789
+ if (u.origin !== location.origin) img.remove();
790
+ } catch {
791
+ img.remove();
792
+ }
793
+ }
794
+ });
795
+ clone.querySelectorAll("*").forEach((el) => {
796
+ if (el.tagName.includes("-")) el.remove();
797
+ });
798
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
799
+ const xs = new XMLSerializer();
800
+ const htmlStr = xs.serializeToString(clone);
801
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}"><foreignObject width="100%" height="100%">` + (inlinedCss ? `<style xmlns="http://www.w3.org/1999/xhtml">${escapeForXml(inlinedCss)}</style>` : "") + htmlStr + `</foreignObject></svg>`;
802
+ const blobUrl = URL.createObjectURL(
803
+ new Blob([svg], { type: "image/svg+xml;charset=utf-8" })
804
+ );
805
+ try {
806
+ const img = await loadImage(blobUrl);
807
+ const canvas = document.createElement("canvas");
808
+ canvas.width = w * dpr;
809
+ canvas.height = h * dpr;
810
+ const ctx = canvas.getContext("2d");
811
+ if (!ctx) throw new Error("canvas 2D context unavailable");
812
+ const pageBg = getComputedStyle(document.body).backgroundColor || "#ffffff";
813
+ ctx.fillStyle = pageBg.startsWith("rgba(0, 0, 0, 0)") ? "#ffffff" : pageBg;
814
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
815
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
816
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
817
+ return { data: dataUrl.split(",")[1], format: "jpeg-base64" };
818
+ } finally {
819
+ URL.revokeObjectURL(blobUrl);
820
+ }
821
+ }
822
+ function loadImage(src) {
823
+ return new Promise((resolve, reject) => {
824
+ const img = new Image();
825
+ img.crossOrigin = "anonymous";
826
+ img.onload = () => resolve(img);
827
+ img.onerror = () => reject(new Error("failed to render snapshot SVG"));
828
+ img.src = src;
829
+ });
830
+ }
831
+ function escapeForXml(s) {
832
+ return s.replace(/]]>/g, "]]&gt;").replace(/<\//g, "&lt;/");
833
+ }
834
+ function readInlineableStyles() {
835
+ const chunks = [];
836
+ for (const sheet of Array.from(document.styleSheets)) {
837
+ try {
838
+ const rules = sheet.cssRules;
839
+ for (const rule of Array.from(rules)) chunks.push(rule.cssText);
840
+ } catch {
841
+ }
842
+ }
843
+ return chunks.join("\n");
844
+ }
845
+ async function captureViaHtmlSnapshot() {
846
+ const inlinedCss = readInlineableStyles();
847
+ const clone = document.documentElement.cloneNode(true);
848
+ clone.querySelectorAll('link[rel="stylesheet"]').forEach((el) => el.remove());
849
+ clone.querySelectorAll("script").forEach((el) => el.remove());
850
+ const head = clone.querySelector("head");
851
+ if (head && inlinedCss) {
852
+ const styleEl = document.createElement("style");
853
+ styleEl.setAttribute("data-botim-snapshot", "1");
854
+ styleEl.textContent = inlinedCss;
855
+ head.insertBefore(styleEl, head.firstChild);
856
+ }
857
+ const html = "<!DOCTYPE html>\n" + clone.outerHTML;
858
+ return {
859
+ data: JSON.stringify({
860
+ html,
861
+ viewport: { w: window.innerWidth, h: window.innerHeight },
862
+ url: location.href,
863
+ capturedAt: Date.now()
864
+ }),
865
+ format: "html-snapshot"
866
+ };
867
+ }
868
+ function registerBuiltins(registry, hooks = {}) {
869
+ registry.register("ping", ping);
870
+ registry.register("reload", makeReload(hooks.reload));
871
+ registry.register("dump-state", makeDumpState(hooks.getState));
872
+ registry.register("set-feature-flag", makeSetFlag(hooks.setFeatureFlag));
873
+ registry.register("screenshot", makeScreenshot(hooks.screenshot));
874
+ }
875
+ var ping = () => ({ ok: true, ts: Date.now() });
876
+ function makeReload(reload) {
877
+ return async () => {
878
+ if (!reload) throw new Error("reload hook not registered by host");
879
+ await reload();
880
+ return { reloaded: true };
881
+ };
882
+ }
883
+ function makeDumpState(getState) {
884
+ return async () => {
885
+ if (!getState) throw new Error("getState hook not registered by host");
886
+ const snapshot = await getState();
887
+ const json = safeStringify(snapshot);
888
+ const truncated = json.length > MAX_DUMP_BYTES;
889
+ return {
890
+ state: truncated ? json.slice(0, MAX_DUMP_BYTES) : json,
891
+ truncated,
892
+ bytes: json.length
893
+ };
894
+ };
895
+ }
896
+ function makeSetFlag(setFeatureFlag) {
897
+ return async (args) => {
898
+ if (!setFeatureFlag) throw new Error("setFeatureFlag hook not registered by host");
899
+ const key = args.key;
900
+ if (typeof key !== "string" || key.length === 0) {
901
+ throw new Error("args.key must be a non-empty string");
902
+ }
903
+ if (!("value" in args)) {
904
+ throw new Error("args.value is required");
905
+ }
906
+ await setFeatureFlag(key, args.value);
907
+ return { applied: true, key };
908
+ };
909
+ }
910
+ function makeScreenshot(screenshot) {
911
+ const capture = screenshot ?? defaultDomScreenshot;
912
+ return async () => {
913
+ const result = await capture();
914
+ let data;
915
+ let format;
916
+ if (typeof result === "string") {
917
+ data = result;
918
+ format = "png-base64";
919
+ } else if (result && typeof result.data === "string") {
920
+ data = result.data;
921
+ format = result.format ?? "png-base64";
922
+ } else {
923
+ throw new Error(
924
+ "screenshot hook must return a base64 string or { data, format }"
925
+ );
926
+ }
927
+ if (data.length === 0) throw new Error("screenshot hook returned empty data");
928
+ if (data.length > MAX_SCREENSHOT_BYTES) {
929
+ throw new Error(
930
+ `screenshot ${data.length} bytes exceeds limit ${MAX_SCREENSHOT_BYTES}`
931
+ );
932
+ }
933
+ return { format, data, bytes: data.length };
934
+ };
935
+ }
936
+ function safeStringify(v) {
937
+ try {
938
+ const seen = /* @__PURE__ */ new WeakSet();
939
+ return JSON.stringify(v, function replacer(_k, val) {
940
+ if (typeof val === "bigint") return val.toString() + "n";
941
+ if (typeof val === "function") return `[Function: ${val.name ?? "anonymous"}]`;
942
+ if (typeof val === "object" && val !== null) {
943
+ if (seen.has(val)) return "[Circular]";
944
+ seen.add(val);
945
+ }
946
+ return val;
947
+ }) ?? "";
948
+ } catch (err) {
949
+ return JSON.stringify({ __unserializable: err instanceof Error ? err.message : "unknown" });
950
+ }
951
+ }
952
+
953
+ // src/dedup.ts
954
+ var DEFAULT_WINDOW_MS = 1e3;
955
+ var DEFAULT_MAX_SIGNATURES = 500;
956
+ var DEDUPED_TYPES = ["console", "network", "error"];
957
+ var EventDeduper = class {
958
+ constructor(onSummary, opts = {}) {
959
+ this.entries = /* @__PURE__ */ new Map();
960
+ this.timer = null;
961
+ this.enabled = opts.enabled !== false;
962
+ this.windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
963
+ this.maxSig = opts.maxSignatures ?? DEFAULT_MAX_SIGNATURES;
964
+ this.onSummary = onSummary;
965
+ if (this.enabled) {
966
+ this.timer = setInterval(() => this.sweep(Date.now()), Math.max(250, Math.floor(this.windowMs / 2)));
967
+ }
968
+ }
969
+ /**
970
+ * Returns true if the SDK should emit `ev` to the buffer; false if it's a
971
+ * duplicate within the current window and should be suppressed.
972
+ */
973
+ shouldEmit(ev) {
974
+ if (!this.enabled) return true;
975
+ if (!DEDUPED_TYPES.includes(ev.type)) return true;
976
+ const sig = this.signatureOf(ev);
977
+ const now = ev.ts;
978
+ const existing = this.entries.get(sig);
979
+ if (!existing) {
980
+ this.evictIfFull();
981
+ this.entries.set(sig, {
982
+ type: ev.type,
983
+ level: ev.level,
984
+ firstTs: now,
985
+ lastTs: now,
986
+ count: 1,
987
+ label: this.labelOf(ev),
988
+ lastPayload: ev.payload
989
+ });
990
+ return true;
991
+ }
992
+ if (now - existing.firstTs >= this.windowMs) {
993
+ if (existing.count > 1) this.emitSummary(sig, existing);
994
+ this.entries.set(sig, {
995
+ type: ev.type,
996
+ level: ev.level,
997
+ firstTs: now,
998
+ lastTs: now,
999
+ count: 1,
1000
+ label: this.labelOf(ev),
1001
+ lastPayload: ev.payload
1002
+ });
1003
+ return true;
1004
+ }
1005
+ existing.lastTs = now;
1006
+ existing.count++;
1007
+ existing.lastPayload = ev.payload;
1008
+ return false;
1009
+ }
1010
+ /**
1011
+ * Force-flush any pending rollups (e.g. before stop). After this returns,
1012
+ * the internal map is empty.
1013
+ */
1014
+ flushAll() {
1015
+ for (const [sig, e] of this.entries) {
1016
+ if (e.count > 1) this.emitSummary(sig, e);
1017
+ }
1018
+ this.entries.clear();
1019
+ }
1020
+ stop() {
1021
+ if (this.timer) {
1022
+ clearInterval(this.timer);
1023
+ this.timer = null;
1024
+ }
1025
+ this.flushAll();
1026
+ }
1027
+ // ─────────────────────────────────────────────────────────────────────────
1028
+ sweep(now) {
1029
+ for (const [sig, e] of this.entries) {
1030
+ if (now - e.firstTs >= this.windowMs) {
1031
+ if (e.count > 1) this.emitSummary(sig, e);
1032
+ this.entries.delete(sig);
1033
+ }
1034
+ }
1035
+ }
1036
+ emitSummary(sig, e) {
1037
+ try {
1038
+ this.onSummary({
1039
+ signature: sig,
1040
+ type: e.type,
1041
+ level: e.level,
1042
+ count: e.count - 1,
1043
+ // first occurrence already emitted as a real event
1044
+ firstTs: e.firstTs,
1045
+ lastTs: e.lastTs,
1046
+ label: e.label,
1047
+ examplePayload: e.lastPayload
1048
+ });
1049
+ } catch {
1050
+ }
1051
+ }
1052
+ evictIfFull() {
1053
+ if (this.entries.size < this.maxSig) return;
1054
+ const firstKey = this.entries.keys().next().value;
1055
+ if (typeof firstKey === "string") {
1056
+ const e = this.entries.get(firstKey);
1057
+ if (e && e.count > 1) this.emitSummary(firstKey, e);
1058
+ this.entries.delete(firstKey);
1059
+ }
1060
+ }
1061
+ // ─── Signature & label generators ───────────────────────────────────────
1062
+ // Signatures are STRUCTURAL: they identify "the same kind of event" rather
1063
+ // than the same bytes. So `console.error("x" + Date.now())` collapses by
1064
+ // method + first-token of args; an HTTP 500 on the same URL collapses
1065
+ // regardless of body content.
1066
+ signatureOf(ev) {
1067
+ if (ev.type === "console") {
1068
+ const p = ev.payload;
1069
+ const args = (p.args || []).map(consoleArgToken).join(" ").slice(0, 240);
1070
+ return `c:${p.method}:${args}`;
1071
+ }
1072
+ if (ev.type === "network") {
1073
+ const p = ev.payload;
1074
+ return `n:${p.phase}:${p.method}:${p.url}:${p.status ?? ""}:${p.errorName ?? ""}`;
1075
+ }
1076
+ if (ev.type === "error") {
1077
+ const p = ev.payload;
1078
+ return `e:${p.source}:${p.name ?? ""}:${(p.message ?? "").slice(0, 240)}`;
1079
+ }
1080
+ return `${ev.type}:${ev.level}`;
1081
+ }
1082
+ labelOf(ev) {
1083
+ if (ev.type === "console") {
1084
+ const p = ev.payload;
1085
+ return `console.${p.method}: ${(p.args || []).map(consoleArgToken).join(" ").slice(0, 160)}`;
1086
+ }
1087
+ if (ev.type === "network") {
1088
+ const p = ev.payload;
1089
+ return `${p.method} ${p.url} (${p.phase}${p.status ? " " + p.status : ""})`;
1090
+ }
1091
+ if (ev.type === "error") {
1092
+ const p = ev.payload;
1093
+ return `${p.name ?? "Error"}: ${(p.message ?? "").slice(0, 160)}`;
1094
+ }
1095
+ return ev.type;
1096
+ }
1097
+ };
1098
+ function consoleArgToken(a) {
1099
+ switch (a.k) {
1100
+ case "primitive":
1101
+ return String(a.v);
1102
+ case "string":
1103
+ case "json":
1104
+ return String(a.v).slice(0, 80);
1105
+ case "error":
1106
+ return `${a.name ?? "Error"}:${a.message ?? ""}`;
1107
+ case "undefined":
1108
+ return "undefined";
1109
+ case "function":
1110
+ return `[fn:${a.name ?? "?"}]`;
1111
+ case "circular":
1112
+ return "[Circular]";
1113
+ case "unserializable":
1114
+ return `[unser:${a.reason}]`;
1115
+ default:
1116
+ return "?";
1117
+ }
1118
+ }
1119
+
1120
+ // src/index.ts
1121
+ var DEFAULT_REDACT_HEADERS = [
1122
+ "authorization",
1123
+ "cookie",
1124
+ "set-cookie",
1125
+ "x-api-key",
1126
+ "x-auth-token",
1127
+ "proxy-authorization"
1128
+ ];
1129
+ var DEFAULT_BODY_PATTERNS = [
1130
+ /(?:bearer\s+)?[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/gi,
1131
+ /[A-Za-z0-9_-]{40,}/g
1132
+ ];
1133
+ var DEFAULT_MAX_BODY_BYTES = 4096;
1134
+ var DEFAULT_FLUSH_INTERVAL_MS = 8e3;
1135
+ var DEFAULT_BUFFER_SIZE = 1e3;
1136
+ var DEFAULT_MAX_BATCH_SIZE = 50;
1137
+ var NOOP_HANDLE = {
1138
+ sid: null,
1139
+ async flush() {
1140
+ },
1141
+ async stop() {
1142
+ },
1143
+ registerCommand() {
1144
+ },
1145
+ unregisterCommand() {
1146
+ return false;
1147
+ }
1148
+ };
1149
+ function assertConsent(config, consent) {
1150
+ if (config.env !== "prod") return;
1151
+ const hasHostToken = typeof consent?.hostToken === "string" && consent.hostToken.length > 0;
1152
+ const hasOptIn = consent?.userOptIn === true;
1153
+ if (hasHostToken || hasOptIn) return;
1154
+ throw new BotimConsentError(
1155
+ "prod debug sessions require consent.hostToken or consent.userOptIn === true"
1156
+ );
1157
+ }
1158
+ function assertConfig(config) {
1159
+ if (!config || typeof config !== "object") {
1160
+ throw new BotimConfigError(
1161
+ 'options.config is required (import botimConfig from "virtual:botim/config")',
1162
+ { code: "config-missing" }
1163
+ );
1164
+ }
1165
+ if (typeof config.miniProgramId !== "string" || config.miniProgramId.length === 0) {
1166
+ throw new BotimConfigError("options.config.miniProgramId must be a non-empty string", {
1167
+ code: "config-missing-field"
1168
+ });
1169
+ }
1170
+ if (config.env !== "dev" && config.env !== "uat" && config.env !== "beta" && config.env !== "prod") {
1171
+ throw new BotimConfigError(
1172
+ `options.config.env must be one of dev|uat|beta|prod (got ${JSON.stringify(config.env)})`,
1173
+ { code: "config-invalid-env" }
1174
+ );
1175
+ }
1176
+ }
1177
+ async function enableRemoteDebug(options) {
1178
+ if (options.enabled === false) return NOOP_HANDLE;
1179
+ assertConfig(options.config);
1180
+ assertConsent(options.config, options.consent);
1181
+ const onError = options.onError ?? (() => {
1182
+ });
1183
+ const app = options.app ?? {
1184
+ name: options.config.appName ?? options.config.miniProgramId,
1185
+ version: options.config.appVersion ?? "0.0.0"
1186
+ };
1187
+ const deviceInfo = detectDeviceInfo(app, options.device);
1188
+ const consent = options.consent ?? {};
1189
+ const session = await attachDevice(options.endpoint, options.config, deviceInfo, consent);
1190
+ const buffer = new RingBuffer({
1191
+ capacity: options.bufferSize ?? DEFAULT_BUFFER_SIZE
1192
+ });
1193
+ const transport = new Transport({
1194
+ ingestUrl: session.ingestUrl,
1195
+ deviceToken: session.deviceToken,
1196
+ buffer,
1197
+ flushIntervalMs: options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
1198
+ maxBatchSize: options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
1199
+ maxRetries: options.maxRetries,
1200
+ onError
1201
+ });
1202
+ const registry = new CommandRegistry();
1203
+ registerBuiltins(registry, options.builtins);
1204
+ let seq = 0;
1205
+ const baseMeta = {
1206
+ appVersion: app.version,
1207
+ build: app.build,
1208
+ deviceId: deviceInfo.deviceId
1209
+ };
1210
+ const sampleConsole = options.sampling?.console ?? 1;
1211
+ const sampleNetwork = options.sampling?.network ?? 1;
1212
+ const redactHeaderList = options.redact?.headers ?? DEFAULT_REDACT_HEADERS;
1213
+ const bodyPatterns = options.redact?.bodyPatterns ?? DEFAULT_BODY_PATTERNS;
1214
+ const maxBodyBytes = options.redact?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
1215
+ const redactNetwork = (p) => {
1216
+ if (p.reqHeaders) p.reqHeaders = redactHeaders(p.reqHeaders, redactHeaderList);
1217
+ if (p.resHeaders) p.resHeaders = redactHeaders(p.resHeaders, redactHeaderList);
1218
+ if (p.reqBody !== void 0) {
1219
+ const r = redactBody(p.reqBody, bodyPatterns, maxBodyBytes);
1220
+ p.reqBody = r.body;
1221
+ p.reqBodyTruncated = r.truncated;
1222
+ }
1223
+ if (p.resBody !== void 0) {
1224
+ const r = redactBody(p.resBody, bodyPatterns, maxBodyBytes);
1225
+ p.resBody = r.body;
1226
+ p.resBodyTruncated = r.truncated;
1227
+ }
1228
+ return p;
1229
+ };
1230
+ const redactConsoleArgs = (p) => {
1231
+ for (const arg of p.args) {
1232
+ if (arg.k === "string" || arg.k === "json") {
1233
+ const r = redactBody(arg.v, bodyPatterns, maxBodyBytes);
1234
+ arg.v = r.body ?? "";
1235
+ if (r.truncated) arg.truncated = true;
1236
+ }
1237
+ }
1238
+ return p;
1239
+ };
1240
+ const deduper = options.dedup === false ? null : new EventDeduper((s) => {
1241
+ const rollupPayload = s.type === "console" ? {
1242
+ method: s.examplePayload?.method ?? "info",
1243
+ args: [
1244
+ {
1245
+ k: "string",
1246
+ v: `[dedup] ${s.label} \u2014 repeated ${s.count} more time${s.count === 1 ? "" : "s"} in ${s.lastTs - s.firstTs}ms`
1247
+ }
1248
+ ]
1249
+ } : s.examplePayload;
1250
+ const ev = {
1251
+ v: SCHEMA_VERSION,
1252
+ sid: session.sid,
1253
+ seq: ++seq,
1254
+ ts: s.lastTs,
1255
+ type: s.type,
1256
+ level: s.level,
1257
+ payload: rollupPayload,
1258
+ meta: {
1259
+ ...baseMeta,
1260
+ dedup: {
1261
+ signature: s.signature,
1262
+ count: s.count,
1263
+ firstTs: s.firstTs,
1264
+ lastTs: s.lastTs
1265
+ }
1266
+ }
1267
+ };
1268
+ buffer.push(ev);
1269
+ }, options.dedup || void 0);
1270
+ const pushOrSuppress = (ev) => {
1271
+ if (!deduper || deduper.shouldEmit(ev)) buffer.push(ev);
1272
+ };
1273
+ const emitConsole = (level, payload) => {
1274
+ const ev = {
1275
+ v: SCHEMA_VERSION,
1276
+ sid: session.sid,
1277
+ seq: ++seq,
1278
+ ts: Date.now(),
1279
+ type: "console",
1280
+ level,
1281
+ payload: redactConsoleArgs(payload),
1282
+ meta: baseMeta
1283
+ };
1284
+ pushOrSuppress(ev);
1285
+ };
1286
+ const emitError = (payload) => {
1287
+ const ev = {
1288
+ v: SCHEMA_VERSION,
1289
+ sid: session.sid,
1290
+ seq: ++seq,
1291
+ ts: Date.now(),
1292
+ type: "error",
1293
+ level: "error",
1294
+ payload,
1295
+ meta: baseMeta
1296
+ };
1297
+ pushOrSuppress(ev);
1298
+ };
1299
+ const emitNetwork = (payload) => {
1300
+ const ev = {
1301
+ v: SCHEMA_VERSION,
1302
+ sid: session.sid,
1303
+ seq: ++seq,
1304
+ ts: Date.now(),
1305
+ type: "network",
1306
+ level: payload.phase === "error" ? "error" : "info",
1307
+ payload: redactNetwork(payload),
1308
+ meta: baseMeta
1309
+ };
1310
+ pushOrSuppress(ev);
1311
+ };
1312
+ const emitCommandResult = (req, kind, payload) => {
1313
+ const ev = {
1314
+ v: SCHEMA_VERSION,
1315
+ sid: session.sid,
1316
+ seq: ++seq,
1317
+ ts: Date.now(),
1318
+ type: kind,
1319
+ level: kind === "command-rejected" ? "warn" : "info",
1320
+ payload,
1321
+ meta: { ...baseMeta, commandId: req.id }
1322
+ };
1323
+ buffer.push(ev);
1324
+ };
1325
+ registry.onOverride((name) => {
1326
+ emitCommandResult(
1327
+ { id: "override-" + name + "-" + Date.now()},
1328
+ "command-rejected",
1329
+ { command: name, reason: "overridden" }
1330
+ );
1331
+ });
1332
+ const uninstallConsole = installConsoleInterceptor({
1333
+ emit: emitConsole,
1334
+ sample: sampleConsole
1335
+ });
1336
+ const uninstallErrors = installErrorInterceptor({ emit: emitError });
1337
+ const internalEndpoints = [session.ingestUrl, session.commandPollUrl];
1338
+ const uninstallNetwork = installNetworkInterceptor({
1339
+ emit: emitNetwork,
1340
+ sample: sampleNetwork,
1341
+ shouldSkip: (url) => internalEndpoints.some((ep) => ep && url.startsWith(ep))
1342
+ });
1343
+ transport.start();
1344
+ transport.startCommandLoop({
1345
+ commandPollUrl: session.commandPollUrl,
1346
+ onCommand: async (req) => {
1347
+ const ctl = new AbortController();
1348
+ const result = await registry.dispatch(req, ctl.signal);
1349
+ emitCommandResult(req, result.kind === "ack" ? "command-ack" : "command-rejected", result.payload);
1350
+ }
1351
+ });
1352
+ return {
1353
+ sid: session.sid,
1354
+ async flush() {
1355
+ await transport.flush();
1356
+ },
1357
+ async stop() {
1358
+ uninstallConsole();
1359
+ uninstallErrors();
1360
+ uninstallNetwork();
1361
+ if (deduper) deduper.stop();
1362
+ await transport.stop();
1363
+ },
1364
+ registerCommand(name, handler) {
1365
+ registry.register(name, handler);
1366
+ },
1367
+ unregisterCommand(name) {
1368
+ return registry.unregister(name);
1369
+ }
1370
+ };
1371
+ }
1372
+
1373
+ export { BotimConfigError, BotimConsentError, DEFAULT_BODY_PATTERNS, DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BODY_BYTES, DEFAULT_REDACT_HEADERS, SCHEMA_VERSION, enableRemoteDebug };
1374
+ //# sourceMappingURL=index.js.map
1375
+ //# sourceMappingURL=index.js.map