@buoy-gg/console 7.0.41 → 7.0.43

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.
Files changed (108) hide show
  1. package/README.md +11 -69
  2. package/lib/commonjs/components/ConsoleIcon.js +1 -19
  3. package/lib/commonjs/components/ConsoleLogList.js +1 -152
  4. package/lib/commonjs/components/ConsoleModal.js +1 -48
  5. package/lib/commonjs/components/ConsoleRoot.js +1 -37
  6. package/lib/commonjs/devtools/ConsoleBody.js +1 -234
  7. package/lib/commonjs/devtools/ConsoleHeader.js +1 -114
  8. package/lib/commonjs/devtools/ConsoleMessageRow.js +1 -356
  9. package/lib/commonjs/devtools/ConsoleValue.js +1 -254
  10. package/lib/commonjs/devtools/ConsoleView.js +1 -54
  11. package/lib/commonjs/devtools/consoleFilter.js +1 -125
  12. package/lib/commonjs/devtools/consoleFormat.js +1 -313
  13. package/lib/commonjs/devtools/origin.js +1 -191
  14. package/lib/commonjs/devtools/theme.js +1 -152
  15. package/lib/commonjs/devtools/useConsoleController.js +1 -82
  16. package/lib/commonjs/devtools/useConsoleMessages.js +1 -241
  17. package/lib/commonjs/hooks/useConsoleLogs.js +1 -23
  18. package/lib/commonjs/index.js +1 -181
  19. package/lib/commonjs/preset.js +1 -63
  20. package/lib/commonjs/store/consoleLogStore.js +6 -589
  21. package/lib/commonjs/store/sanitize.js +1 -83
  22. package/lib/commonjs/sync/consoleSyncAdapter.js +1 -86
  23. package/lib/module/components/ConsoleIcon.js +1 -12
  24. package/lib/module/components/ConsoleLogList.js +1 -147
  25. package/lib/module/components/ConsoleModal.js +1 -44
  26. package/lib/module/components/ConsoleRoot.js +1 -33
  27. package/lib/module/devtools/ConsoleBody.js +1 -229
  28. package/lib/module/devtools/ConsoleHeader.js +1 -109
  29. package/lib/module/devtools/ConsoleMessageRow.js +1 -352
  30. package/lib/module/devtools/ConsoleValue.js +1 -249
  31. package/lib/module/devtools/ConsoleView.js +1 -49
  32. package/lib/module/devtools/consoleFilter.js +1 -119
  33. package/lib/module/devtools/consoleFormat.js +1 -309
  34. package/lib/module/devtools/origin.js +1 -183
  35. package/lib/module/devtools/theme.js +1 -143
  36. package/lib/module/devtools/useConsoleController.js +1 -79
  37. package/lib/module/devtools/useConsoleMessages.js +1 -238
  38. package/lib/module/hooks/useConsoleLogs.js +1 -19
  39. package/lib/module/index.js +1 -52
  40. package/lib/module/preset.js +1 -61
  41. package/lib/module/store/consoleLogStore.js +6 -575
  42. package/lib/module/store/sanitize.js +1 -77
  43. package/lib/module/sync/consoleSyncAdapter.js +1 -82
  44. package/lib/web/index.mjs +11 -12
  45. package/package.json +3 -6
  46. package/lib/commonjs/components/ConsoleIcon.js.map +0 -1
  47. package/lib/commonjs/components/ConsoleLogList.js.map +0 -1
  48. package/lib/commonjs/components/ConsoleModal.js.map +0 -1
  49. package/lib/commonjs/components/ConsoleRoot.js.map +0 -1
  50. package/lib/commonjs/devtools/ConsoleBody.js.map +0 -1
  51. package/lib/commonjs/devtools/ConsoleHeader.js.map +0 -1
  52. package/lib/commonjs/devtools/ConsoleMessageRow.js.map +0 -1
  53. package/lib/commonjs/devtools/ConsoleValue.js.map +0 -1
  54. package/lib/commonjs/devtools/ConsoleView.js.map +0 -1
  55. package/lib/commonjs/devtools/consoleFilter.js.map +0 -1
  56. package/lib/commonjs/devtools/consoleFormat.js.map +0 -1
  57. package/lib/commonjs/devtools/origin.js.map +0 -1
  58. package/lib/commonjs/devtools/theme.js.map +0 -1
  59. package/lib/commonjs/devtools/useConsoleController.js.map +0 -1
  60. package/lib/commonjs/devtools/useConsoleMessages.js.map +0 -1
  61. package/lib/commonjs/hooks/useConsoleLogs.js.map +0 -1
  62. package/lib/commonjs/index.js.map +0 -1
  63. package/lib/commonjs/preset.js.map +0 -1
  64. package/lib/commonjs/store/consoleLogStore.js.map +0 -1
  65. package/lib/commonjs/store/sanitize.js.map +0 -1
  66. package/lib/commonjs/sync/consoleSyncAdapter.js.map +0 -1
  67. package/lib/module/components/ConsoleIcon.js.map +0 -1
  68. package/lib/module/components/ConsoleLogList.js.map +0 -1
  69. package/lib/module/components/ConsoleModal.js.map +0 -1
  70. package/lib/module/components/ConsoleRoot.js.map +0 -1
  71. package/lib/module/devtools/ConsoleBody.js.map +0 -1
  72. package/lib/module/devtools/ConsoleHeader.js.map +0 -1
  73. package/lib/module/devtools/ConsoleMessageRow.js.map +0 -1
  74. package/lib/module/devtools/ConsoleValue.js.map +0 -1
  75. package/lib/module/devtools/ConsoleView.js.map +0 -1
  76. package/lib/module/devtools/consoleFilter.js.map +0 -1
  77. package/lib/module/devtools/consoleFormat.js.map +0 -1
  78. package/lib/module/devtools/origin.js.map +0 -1
  79. package/lib/module/devtools/theme.js.map +0 -1
  80. package/lib/module/devtools/useConsoleController.js.map +0 -1
  81. package/lib/module/devtools/useConsoleMessages.js.map +0 -1
  82. package/lib/module/hooks/useConsoleLogs.js.map +0 -1
  83. package/lib/module/index.js.map +0 -1
  84. package/lib/module/preset.js.map +0 -1
  85. package/lib/module/store/consoleLogStore.js.map +0 -1
  86. package/lib/module/store/sanitize.js.map +0 -1
  87. package/lib/module/sync/consoleSyncAdapter.js.map +0 -1
  88. package/lib/typescript/components/ConsoleIcon.d.ts.map +0 -1
  89. package/lib/typescript/components/ConsoleLogList.d.ts.map +0 -1
  90. package/lib/typescript/components/ConsoleModal.d.ts.map +0 -1
  91. package/lib/typescript/components/ConsoleRoot.d.ts.map +0 -1
  92. package/lib/typescript/devtools/ConsoleBody.d.ts.map +0 -1
  93. package/lib/typescript/devtools/ConsoleHeader.d.ts.map +0 -1
  94. package/lib/typescript/devtools/ConsoleMessageRow.d.ts.map +0 -1
  95. package/lib/typescript/devtools/ConsoleValue.d.ts.map +0 -1
  96. package/lib/typescript/devtools/ConsoleView.d.ts.map +0 -1
  97. package/lib/typescript/devtools/consoleFilter.d.ts.map +0 -1
  98. package/lib/typescript/devtools/consoleFormat.d.ts.map +0 -1
  99. package/lib/typescript/devtools/origin.d.ts.map +0 -1
  100. package/lib/typescript/devtools/theme.d.ts.map +0 -1
  101. package/lib/typescript/devtools/useConsoleController.d.ts.map +0 -1
  102. package/lib/typescript/devtools/useConsoleMessages.d.ts.map +0 -1
  103. package/lib/typescript/hooks/useConsoleLogs.d.ts.map +0 -1
  104. package/lib/typescript/index.d.ts.map +0 -1
  105. package/lib/typescript/preset.d.ts.map +0 -1
  106. package/lib/typescript/store/consoleLogStore.d.ts.map +0 -1
  107. package/lib/typescript/store/sanitize.d.ts.map +0 -1
  108. package/lib/typescript/sync/consoleSyncAdapter.d.ts.map +0 -1
@@ -1,591 +1,8 @@
1
- "use strict";
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.clearEntries=clearEntries;exports.getEntries=getEntries;exports.getPreserveLog=getPreserveLog;exports.initConsolePersistence=initConsolePersistence;exports.installConsoleCapture=installConsoleCapture;exports.installFatalErrorCapture=installFatalErrorCapture;exports.onClear=onClear;exports.replaceEntries=replaceEntries;exports.setPreserveLog=setPreserveLog;exports.subscribe=subscribe;exports.subscribePreserveLog=subscribePreserveLog;exports.uninstallConsoleCapture=uninstallConsoleCapture;var _sharedUi=require("@buoy-gg/shared-ui");var React=_interopRequireWildcard(require("react"));var _sanitize=require("./sanitize");var _origin=require("../devtools/origin");function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var n=new WeakMap,o=new WeakMap;return(_interopRequireWildcard=function(r,s){if(!s&&r&&r.__esModule)return r;var i,a,l={__proto__:null,default:r};if(null===r||"object"!=typeof r&&"function"!=typeof r)return l;if(i=s?o:n){if(i.has(r))return i.get(r);i.set(r,l)}for(const c in r)"default"!==c&&{}.hasOwnProperty.call(r,c)&&((a=(i=Object.defineProperty)&&Object.getOwnPropertyDescriptor(r,c))&&(a.get||a.set)?i(l,c,a):l[c]=r[c]);return l})(e,t)}const METHODS=["log","info","warn","error","debug","trace","dir","table","assert","group","groupCollapsed","groupEnd"];const STACK_METHODS=new Set(["error","warn","trace","assert"]);const METHOD_LEVEL={error:"error",assert:"error",warn:"warning",debug:"verbose",log:"info",info:"info",trace:"info",dir:"info",table:"info",group:"info",groupCollapsed:"info",groupEnd:"info"};const MAX_ENTRIES=1e3;const store=(0,_sharedUi.createToolStore)({id:"console",max:MAX_ENTRIES,order:"append",coalesce:"frame"});let installed=false;let fatalInstalled=false;let captureDisabledByUser=false;const originals={};function formatArg(e){if(typeof e==="string")return e;if(e instanceof Error)return e.stack??`${e.name}: ${e.message}`;if(e===void 0)return"undefined";if(e===null)return"null";if(typeof e==="function")return`\u0192 ${e.name||"(anonymous)"}`;try{return JSON.stringify(e)}catch{return String(e)}}function formatArgs(e){return e.map(formatArg).join(" ")}function cleanStack(e){if(!e)return void 0;return e.split("\n").slice(3).join("\n")||void 0}function captureOwnerStack(){try{const e=Reflect.get(React,"captureOwnerStack");return typeof e==="function"?e()??void 0:void 0}catch{return void 0}}const COMPONENT_STACK_FRAMES=8;function compactComponentStack(e){const t=e.split("\n").map(r=>r.trim()).filter(Boolean).map(r=>r.replace(/\s*\((?:https?:|\/).*$/,""));const n=t.slice(0,COMPONENT_STACK_FRAMES);const o=t.length-n.length;return n.map(r=>` ${r}`).join("\n")+(o>0?`
2
+ \u2026+${o} more frames`:"")}function asComponentError(e){if(!(e instanceof Error))return null;const t=e;return t.isComponentError||typeof t.componentStack==="string"?t:null}function record(e,t){const n=new Error().stack;const o=(0,_origin.parseOrigin)(n,captureOwnerStack());const r=e==="error"?asComponentError(t[0]):null;if(r){const i=fatalKey(r.name,r.message);const a=r.componentStack?`
2
3
 
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.clearEntries = clearEntries;
7
- exports.getEntries = getEntries;
8
- exports.getPreserveLog = getPreserveLog;
9
- exports.initConsolePersistence = initConsolePersistence;
10
- exports.installConsoleCapture = installConsoleCapture;
11
- exports.installFatalErrorCapture = installFatalErrorCapture;
12
- exports.onClear = onClear;
13
- exports.replaceEntries = replaceEntries;
14
- exports.setPreserveLog = setPreserveLog;
15
- exports.subscribe = subscribe;
16
- exports.subscribePreserveLog = subscribePreserveLog;
17
- exports.uninstallConsoleCapture = uninstallConsoleCapture;
18
- var _sharedUi = require("@buoy-gg/shared-ui");
19
- var React = _interopRequireWildcard(require("react"));
20
- var _sanitize = require("./sanitize");
21
- var _origin = require("../devtools/origin");
22
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
23
- /**
24
- * consoleLogStore
25
- *
26
- * Intercepts the global `console` methods and keeps a bounded ring buffer of
27
- * captured entries, modeled after Chrome DevTools' ConsoleMessage. Direct
28
- * console patching is the capture path (works identically on Expo, RN CLI, and
29
- * web). The DevTools-style view layer (filtering, formatting, grouping) is built
30
- * on top of these raw entries.
31
- *
32
- * By default the buffer is in-memory only, so a JS reload starts fresh — just
33
- * like the Metro/Chrome console. A "Preserve log" toggle persists the buffer to
34
- * storage and restores it on the next launch (Chrome's "Preserve log").
35
- */
4
+ This error is located at:
5
+ ${compactComponentStack(r.componentStack)}`:"";pushFatal({id:store.allocId(),method:"error",level:"error",args:["[RENDER ERROR]",r],message:`[RENDER ERROR] ${i}${a}`,timestamp:Date.now(),stack:r.stack,origin:o},isDuplicateFatal(`[RENDER ERROR] ${i}`));return}const s={id:store.allocId(),method:e,level:METHOD_LEVEL[e],args:t,message:formatArgs(t),timestamp:Date.now(),stack:STACK_METHODS.has(e)?cleanStack(n):void 0,origin:o};store.add(s);scheduleSave();if(s.level==="error")store.flushSync()}const FATAL_DEDUPE_MS=2e3;let lastFatalKey="";let lastFatalAt=0;function fatalKey(e,t){const n=t.trim();if(e&&n.startsWith(`${e}: `))return n;return e?`${e}: ${n}`:n}function isDuplicateFatal(e){const t=Date.now();if(e===lastFatalKey&&t-lastFatalAt<FATAL_DEDUPE_MS)return true;lastFatalKey=e;lastFatalAt=t;return false}function pushFatal(e,t=false){store.add(e);scheduleSave();if(!t)store.flushSync(true)}function recordFatalError(e,t){const n=e instanceof Error?e:void 0;const o=t===false?"[UNCAUGHT]":"[FATAL]";const r=n?fatalKey(n.name,n.message):formatArg(e);const s=isDuplicateFatal(`${o} ${r}`);pushFatal({id:store.allocId(),method:"error",level:"error",args:[o,e],message:`${o} ${r}`,timestamp:Date.now(),stack:n?.stack,fatal:true},s)}function formatNativeStack(e){const t=e.extraData?.rawStack;if(typeof t==="string"&&t.length>0)return t;if(!Array.isArray(e.stack)||e.stack.length===0)return void 0;return e.stack.map(n=>{const o=n.lineNumber!=null?`${n.file??"?"}:${n.lineNumber}:${n.column??0}`:n.file??"?";return` at ${n.methodName||"<anonymous>"} (${o})`}).join("\n")}function recordNativeException(e){const t=e.isFatal===false?"[UNCAUGHT]":"[FATAL]";const n=fatalKey(e.name??void 0,e.originalMessage??e.message??"Unknown error");const o=isDuplicateFatal(`${t} ${n}`);const r=typeof e.componentStack==="string"&&e.componentStack.trim()?`
36
6
 
37
- /** The console method that produced an entry. */
38
-
39
- /** DevTools log levels (Protocol.Log.LogEntryLevel). */
40
-
41
- /** Methods we intercept, in the order we install them. */
42
- const METHODS = ["log", "info", "warn", "error", "debug", "trace", "dir", "table", "assert", "group", "groupCollapsed", "groupEnd"];
43
-
44
- /** Methods whose call site we capture a stack for. */
45
- const STACK_METHODS = new Set(["error", "warn", "trace", "assert"]);
46
-
47
- /** method → DevTools level. */
48
- const METHOD_LEVEL = {
49
- error: "error",
50
- assert: "error",
51
- warn: "warning",
52
- debug: "verbose",
53
- log: "info",
54
- info: "info",
55
- trace: "info",
56
- dir: "info",
57
- table: "info",
58
- group: "info",
59
- groupCollapsed: "info",
60
- groupEnd: "info"
61
- };
62
-
63
- /** Max entries retained before the oldest are dropped. */
64
- const MAX_ENTRIES = 1000;
65
- // Frame-coalesced emit: console traffic is inherently bursty (loops,
66
- // per-frame logs), and uncoalesced each entry was its own React commit —
67
- // the console tool measured 41 JS FPS / 84% CPU / 7 jank during a bench
68
- // dwell (tool-bench 2026-07-06) largely from its own live churn. The buffer,
69
- // listeners, clear-forwarding and the "push to the dashboard NOW" hook are
70
- // the shared `createToolStore`; this module adds capture + persistence.
71
- const store = (0, _sharedUi.createToolStore)({
72
- id: "console",
73
- max: MAX_ENTRIES,
74
- order: "append",
75
- coalesce: "frame"
76
- });
77
- let installed = false;
78
- let fatalInstalled = false;
79
- /**
80
- * Raised by an explicit `uninstallConsoleCapture()`. The fatal-error hook can't
81
- * be safely unwound once chained (another library may have chained on top of
82
- * ours since), so an explicit user OFF mutes fatal recording instead.
83
- */
84
- let captureDisabledByUser = false;
85
-
86
- /** Saved originals so we can restore on uninstall and still print through. */
87
- const originals = {};
88
-
89
- /** Best-effort, single-line serialization of a single console argument. */
90
- function formatArg(arg) {
91
- if (typeof arg === "string") return arg;
92
- if (arg instanceof Error) return arg.stack ?? `${arg.name}: ${arg.message}`;
93
- if (arg === undefined) return "undefined";
94
- if (arg === null) return "null";
95
- if (typeof arg === "function") return `ƒ ${arg.name || "(anonymous)"}`;
96
- try {
97
- return JSON.stringify(arg);
98
- } catch {
99
- return String(arg);
100
- }
101
- }
102
- function formatArgs(args) {
103
- return args.map(formatArg).join(" ");
104
- }
105
-
106
- /** Drop the "Error" header + the interceptor's own frames from a raw stack. */
107
- function cleanStack(raw) {
108
- if (!raw) return undefined;
109
- return raw.split("\n").slice(3).join("\n") || undefined;
110
- }
111
-
112
- /** Nearest owning React component stack (dev only; empty off-render). */
113
- function captureOwnerStack() {
114
- try {
115
- // React only exposes this API in some development builds. Keep the lookup
116
- // dynamic so browser bundlers do not require a production named export.
117
- const fn = Reflect.get(React, 'captureOwnerStack');
118
- return typeof fn === "function" ? fn() ?? undefined : undefined;
119
- } catch {
120
- return undefined;
121
- }
122
- }
123
-
124
- /**
125
- * An error React Native decorated on its way through ExceptionsManager: a
126
- * render/lifecycle crash carrying the component stack. `isComponentError` is
127
- * set by RN's `getExtendedError`, so this identifies a REAL crash rather than
128
- * an ordinary `console.error("...")` call.
129
- */
130
-
131
- /** Frames to keep from a component stack — the crash site plus its context. */
132
- const COMPONENT_STACK_FRAMES = 8;
133
-
134
- /**
135
- * Squeeze a React component stack down to the part a human (or an agent
136
- * reading this over a wire) actually uses: the component names nearest the
137
- * crash.
138
- *
139
- * Raw component stacks in a Metro dev bundle append the full bundle URL with
140
- * every query param to EVERY frame — one crash is ~15 KB of near-identical
141
- * `http://192.168.x.x:8830/…&transform.routerRoot=src/app…` noise wrapped
142
- * around the handful of names that matter. The complete error, componentStack
143
- * included, is still on the entry's `args` for anyone who wants it.
144
- */
145
- function compactComponentStack(stack) {
146
- const frames = stack.split("\n").map(line => line.trim()).filter(Boolean)
147
- // Drop the "(url)" tail; keep a "Name(./file.tsx)" hint when RN provides one.
148
- .map(line => line.replace(/\s*\((?:https?:|\/).*$/, ""));
149
- const shown = frames.slice(0, COMPONENT_STACK_FRAMES);
150
- const rest = frames.length - shown.length;
151
- return shown.map(f => ` ${f}`).join("\n") + (rest > 0 ? `\n …+${rest} more frames` : "");
152
- }
153
- function asComponentError(arg) {
154
- if (!(arg instanceof Error)) return null;
155
- const e = arg;
156
- return e.isComponentError || typeof e.componentStack === "string" ? e : null;
157
- }
158
- function record(method, args) {
159
- // Capture the call site for every log so we can attribute its origin.
160
- const rawStack = new Error().stack;
161
- const origin = (0, _origin.parseOrigin)(rawStack, captureOwnerStack());
162
-
163
- // React Native funnels an uncaught render error back through console.error
164
- // (ExceptionsManager.reportException) so console monkey-patches still see it.
165
- // That arrives as an ordinary error row, which badly under-sells it: the app
166
- // is dead. Promote it to a tagged [FATAL] entry carrying the component stack.
167
- const crash = method === "error" ? asComponentError(args[0]) : null;
168
- if (crash) {
169
- // Tagged [RENDER ERROR], not [FATAL]: React reports an error an
170
- // ErrorBoundary CAUGHT and recovered from through this same path, with the
171
- // same componentStack, and nothing here distinguishes the two. Claiming a
172
- // healthy app "most likely CRASHED" is worse than under-claiming — the
173
- // component stack, which is the useful part, is reported either way.
174
- const headline = fatalKey(crash.name, crash.message);
175
- const located = crash.componentStack ? `\n\nThis error is located at:\n${compactComponentStack(crash.componentStack)}` : "";
176
- pushFatal({
177
- id: store.allocId(),
178
- method: "error",
179
- level: "error",
180
- args: ["[RENDER ERROR]", crash],
181
- message: `[RENDER ERROR] ${headline}${located}`,
182
- timestamp: Date.now(),
183
- stack: crash.stack,
184
- origin
185
- }, isDuplicateFatal(`[RENDER ERROR] ${headline}`));
186
- return;
187
- }
188
- const entry = {
189
- id: store.allocId(),
190
- method,
191
- level: METHOD_LEVEL[method],
192
- args,
193
- message: formatArgs(args),
194
- timestamp: Date.now(),
195
- stack: STACK_METHODS.has(method) ? cleanStack(rawStack) : undefined,
196
- origin
197
- };
198
- store.add(entry);
199
- scheduleSave();
200
- // Errors are the entries most likely to be the last thing an app ever logs,
201
- // so they get pushed to any dashboard immediately instead of waiting on the
202
- // throttle that a dying tree will never run. Rate-limited; ordinary logs
203
- // still ride the normal throttled path.
204
- if (entry.level === "error") store.flushSync();
205
- }
206
-
207
- /** Minimal shape of React Native's global `ErrorUtils`. */
208
-
209
- /**
210
- * Two independent seams report the same crash on some setups (the C++ handler
211
- * can also feed `console.error`). Suppress a repeat of the same headline within
212
- * this window so a crash reads as ONE entry.
213
- */
214
- const FATAL_DEDUPE_MS = 2000;
215
- let lastFatalKey = "";
216
- let lastFatalAt = 0;
217
-
218
- /**
219
- * Normalize a crash headline so the same error reported through two different
220
- * seams produces the same key. React Native often sets `error.message` to the
221
- * ALREADY-prefixed string ("TypeError: x is not a function"), so one path
222
- * builds "TypeError: TypeError: x…" and the other "TypeError: x…" — which
223
- * defeats the de-duplication entirely.
224
- */
225
- function fatalKey(name, message) {
226
- const body = message.trim();
227
- if (name && body.startsWith(`${name}: `)) return body;
228
- return name ? `${name}: ${body}` : body;
229
- }
230
-
231
- /**
232
- * Has this crash already been recorded moments ago?
233
- *
234
- * Only ever used to suppress a duplicate PUSH to the dashboard — never to drop
235
- * an entry. Two different components crashing with the same message inside the
236
- * window are two crashes, and a tool whose pitch is "the app's last words"
237
- * must not silently throw one away.
238
- */
239
- function isDuplicateFatal(key) {
240
- const now = Date.now();
241
- if (key === lastFatalKey && now - lastFatalAt < FATAL_DEDUPE_MS) return true;
242
- lastFatalKey = key;
243
- lastFatalAt = now;
244
- return false;
245
- }
246
-
247
- /**
248
- * The device sync bridge's "push this tool NOW" escape hatch
249
- * (@buoy-gg/external-sync `crashFlush`). Reached through the global rather than
250
- * an import because console deliberately has no dependency on external-sync.
251
- */
252
- // The flush itself lives on the shared store (`store.flushSync`): rate-limited
253
- // unless forced — a fatal is the app's last word and always goes out.
254
-
255
- /**
256
- * Append a pre-built fatal entry to the ring buffer, then push it to any
257
- * connected dashboard IMMEDIATELY.
258
- *
259
- * A fatal render error unmounts the React tree microseconds later, taking the
260
- * sync bridge — and the normal 200 ms throttled snapshot — with it. Flushing
261
- * synchronously here is what makes a crashed app's last words readable from
262
- * the desktop app and the MCP server instead of the app just going silent.
263
- */
264
- function pushFatal(entry, duplicate = false) {
265
- store.add(entry);
266
- scheduleSave();
267
- // A repeat within the dedupe window is still RECORDED (two crashes are two
268
- // crashes); it just doesn't earn another forced push to the dashboard.
269
- if (!duplicate) store.flushSync(true);
270
- }
271
-
272
- /** Record an uncaught JS error from the global handler as an error entry. */
273
- function recordFatalError(error, isFatal) {
274
- const err = error instanceof Error ? error : undefined;
275
- // RN passes isFatal:false for non-fatal uncaught errors (e.g. errors thrown
276
- // in event handlers on some setups); anything else is treated as fatal.
277
- const label = isFatal === false ? "[UNCAUGHT]" : "[FATAL]";
278
- const headline = err ? fatalKey(err.name, err.message) : formatArg(error);
279
- const duplicate = isDuplicateFatal(`${label} ${headline}`);
280
- // The label leads the args (not just the pre-rendered message): the
281
- // DevTools-style row renders from args, so this is what makes the tag
282
- // visible in the UI — exactly like `console.error("[FATAL]", err)`.
283
- pushFatal({
284
- id: store.allocId(),
285
- method: "error",
286
- level: "error",
287
- args: [label, error],
288
- message: `${label} ${headline}`,
289
- timestamp: Date.now(),
290
- // The error's OWN stack — the crash site, not this handler's call site.
291
- stack: err?.stack,
292
- fatal: true
293
- }, duplicate);
294
- }
295
-
296
- /**
297
- * The exception payload React Native's C++ error handler hands to listeners
298
- * registered via `global.RN$registerExceptionListener` (JsErrorHandler.cpp).
299
- */
300
-
301
- /** Render the C++ handler's parsed frames back into a normal stack string. */
302
- function formatNativeStack(data) {
303
- const raw = data.extraData?.rawStack;
304
- if (typeof raw === "string" && raw.length > 0) return raw;
305
- if (!Array.isArray(data.stack) || data.stack.length === 0) return undefined;
306
- return data.stack.map(f => {
307
- const where = f.lineNumber != null ? `${f.file ?? "?"}:${f.lineNumber}:${f.column ?? 0}` : f.file ?? "?";
308
- return ` at ${f.methodName || "<anonymous>"} (${where})`;
309
- }).join("\n");
310
- }
311
-
312
- /** Record a crash reported by React Native's native (C++) error handler. */
313
- function recordNativeException(data) {
314
- const label = data.isFatal === false ? "[UNCAUGHT]" : "[FATAL]";
315
- const headline = fatalKey(data.name ?? undefined, data.originalMessage ?? data.message ?? "Unknown error");
316
- const duplicate = isDuplicateFatal(`${label} ${headline}`);
317
- // The component stack is the single most useful line for "which component
318
- // crashed" — RN puts it in the redbox, so put it in the console entry too.
319
- const componentStack = typeof data.componentStack === "string" && data.componentStack.trim() ? `\n\nThis error is located at:\n${compactComponentStack(data.componentStack)}` : "";
320
- const message = `${label} ${headline}${componentStack}`;
321
- pushFatal({
322
- id: store.allocId(),
323
- method: "error",
324
- level: "error",
325
- args: [label, headline, ...(componentStack ? [componentStack.trim()] : [])],
326
- message,
327
- timestamp: Date.now(),
328
- stack: formatNativeStack(data),
329
- fatal: true
330
- }, duplicate);
331
- }
332
-
333
- /**
334
- * Register with React Native's native (C++) exception handler — the seam a
335
- * RENDER crash actually travels through on the New Architecture.
336
- *
337
- * React 19 routes an uncaught render error to `onUncaughtError` →
338
- * `ExceptionsManager.handleException`, which NEVER reaches
339
- * `ErrorUtils.setGlobalHandler`. Where it goes from there depends on the build:
340
- *
341
- * - `useAlwaysAvailableJSErrorHandling` on: the C++ handler
342
- * (`global.RN$handleException`) reports it and JS sees nothing — this
343
- * listener is the only seam.
344
- * - otherwise: JS `reportException` re-feeds `console.error(e)` "so any methods
345
- * monkey patched on top of console.error are called", which the console patch
346
- * catches and promotes to `[FATAL]` (see `record`).
347
- *
348
- * Both are covered, and a crash reported through both de-duplicates to one
349
- * entry. `RN$registerExceptionListener` keeps a LIST of listeners (LogBox
350
- * registers one too), so this is purely additive. We never call the payload's
351
- * `preventDefault()`: suppressing RN's own redbox is not our call to make.
352
- */
353
- function installNativeExceptionCapture() {
354
- const register = globalThis.RN$registerExceptionListener;
355
- if (typeof register !== "function") return false;
356
- register(data => {
357
- try {
358
- if (!captureDisabledByUser) recordNativeException(data);
359
- } catch {
360
- // Capture must never interfere with error reporting.
361
- }
362
- });
363
- return true;
364
- }
365
-
366
- /**
367
- * Hook React Native's error paths so FATAL/uncaught JS errors land in the
368
- * console store: the classic `ErrorUtils.setGlobalHandler` (old architecture,
369
- * native-module callbacks, promise rejections) AND the native C++ exception
370
- * listener (New Architecture render crashes — see
371
- * {@link installNativeExceptionCapture}). Both feed the same de-duplicated
372
- * `[FATAL]` entry, so a crash reported through both seams shows up once.
373
- *
374
- * - Chains the previous handler (LogBox/redbox still fire; nothing swallowed).
375
- * - Idempotent, and a silent no-op off React Native (neither global present) —
376
- * which also keeps it inert on the desktop dashboard / web mirrors.
377
- * - Recording respects an explicit `uninstallConsoleCapture()` (user OFF), but
378
- * the hooks themselves stay chained — they can't be safely removed.
379
- */
380
- function installFatalErrorCapture() {
381
- if (fatalInstalled) return;
382
- // Latch only once a seam is actually attached. Setting the flag up front
383
- // means an install that runs before `InitializeCore` has published
384
- // `ErrorUtils` would latch anyway and never retry — silently losing promise
385
- // rejections and native-callback errors, which travel ONLY that seam.
386
- const nativeInstalled = installNativeExceptionCapture();
387
- const errorUtils = globalThis.ErrorUtils;
388
- if (typeof errorUtils?.setGlobalHandler !== "function") {
389
- fatalInstalled = nativeInstalled;
390
- return;
391
- }
392
- fatalInstalled = true;
393
- const previous = typeof errorUtils.getGlobalHandler === "function" ? errorUtils.getGlobalHandler() : undefined;
394
- errorUtils.setGlobalHandler((error, isFatal) => {
395
- try {
396
- if (!captureDisabledByUser) recordFatalError(error, isFatal);
397
- } catch {
398
- // Capture must never interfere with error reporting.
399
- }
400
- if (typeof previous === "function") previous(error, isFatal);
401
- });
402
- }
403
-
404
- /**
405
- * Patch the global console methods. Idempotent — safe to call more than once.
406
- * Each patched method still forwards to the original so normal logging keeps
407
- * working (Metro terminal, Chrome, native logcat/Xcode).
408
- *
409
- * Also installs the fatal-error hook (see `installFatalErrorCapture`) so every
410
- * install path captures crashes, not just `console.*` calls.
411
- */
412
- function installConsoleCapture() {
413
- if ((0, _sharedUi.getAccountAccess)().status === "locked") return;
414
- captureDisabledByUser = false;
415
- installFatalErrorCapture();
416
- if (installed) return;
417
- installed = true;
418
- for (const method of METHODS) {
419
- const original = console[method];
420
- if (typeof original !== "function") continue;
421
- originals[method] = original;
422
- // Named so origin parsing can identify (and skip) this frame by function
423
- // name — in a Metro bundle every frame shares one bundle URL, so filename
424
- // filtering can't distinguish our wrapper from the real caller.
425
- console[method] = function buoyConsoleProxy(...args) {
426
- record(method, args);
427
- original.apply(console, args);
428
- };
429
- }
430
- }
431
-
432
- /** Restore the original console methods. Also mutes fatal-error recording
433
- * (the hook itself stays chained — see `installFatalErrorCapture`). */
434
- function uninstallConsoleCapture() {
435
- captureDisabledByUser = true;
436
- if (!installed) return;
437
- installed = false;
438
- for (const method of METHODS) {
439
- const original = originals[method];
440
- if (original) console[method] = original;
441
- delete originals[method];
442
- }
443
- }
444
-
445
- /** Subscribe to changes. Returns an unsubscribe function. */
446
- function subscribe(listener) {
447
- return store.subscribe(listener);
448
- }
449
-
450
- /** Current snapshot (stable reference between mutations for useSyncExternalStore). */
451
- function getEntries() {
452
- return store.getSnapshot();
453
- }
454
-
455
- /** Clear all captured entries. Fires onClear listeners (e.g. to forward the
456
- * clear to a remote device when running in mirror mode). */
457
- function clearEntries() {
458
- void clearSavedBuffer();
459
- store.clear();
460
- }
461
-
462
- // ─── Remote-mirror mode (desktop dashboard) ──────────────────────────────────
463
- // The desktop imports this same store but, instead of patching its own console,
464
- // drives it from device snapshots via replaceEntries(). Capture is never
465
- // installed there, so local and remote modes never mix.
466
-
467
- /** Replace the whole entry list from a remote snapshot. */
468
- function replaceEntries(next) {
469
- store.set(next);
470
- }
471
-
472
- /** Register a listener fired when clearEntries() is called. Returns an
473
- * unsubscribe function. Used by the dashboard to forward clears to the device. */
474
- function onClear(listener) {
475
- return store.onClear(listener);
476
- }
477
-
478
- // ─── Preserve log (persist across reload) ────────────────────────────────────
479
- // Default OFF: the buffer is in-memory only, so a reload starts fresh. When ON,
480
- // the buffer is saved to storage (debounced) and restored on the next launch.
481
-
482
- const PRESERVE_KEY = "@react_buoy_console_preserve";
483
- const BUFFER_KEY = "@react_buoy_console_buffer";
484
- const SAVE_DEBOUNCE_MS = 600;
485
- let preserveLog = false;
486
- let persistenceReady = false;
487
- let persistenceGeneration = 0;
488
- let bufferWrites = Promise.resolve();
489
- const preserveListeners = new Set();
490
- let saveTimer = null;
491
- function emitPreserve() {
492
- for (const listener of preserveListeners) listener();
493
- }
494
- function scheduleSave() {
495
- if (!preserveLog || !(0, _sharedUi.hasAccountAccess)()) return;
496
- if (saveTimer) clearTimeout(saveTimer);
497
- saveTimer = setTimeout(saveBuffer, SAVE_DEBOUNCE_MS);
498
- }
499
- async function saveBuffer() {
500
- saveTimer = null;
501
- if (!preserveLog || !(0, _sharedUi.hasAccountAccess)()) return;
502
- try {
503
- const serializable = store.getSnapshot().map(entry => ({
504
- ...entry,
505
- args: (0, _sanitize.sanitizeArgs)(entry.args)
506
- }));
507
- const generation = persistenceGeneration;
508
- bufferWrites = bufferWrites.then(async () => {
509
- if (generation !== persistenceGeneration || !preserveLog || !(0, _sharedUi.hasAccountAccess)()) return;
510
- await _sharedUi.persistentStorage.setItem(BUFFER_KEY, JSON.stringify(serializable));
511
- }).catch(() => {});
512
- await bufferWrites;
513
- } catch {
514
- // Storage unavailable — preserve is best-effort.
515
- }
516
- }
517
- async function clearSavedBuffer() {
518
- persistenceGeneration++;
519
- persistenceReady = false;
520
- try {
521
- bufferWrites = bufferWrites.then(() => _sharedUi.persistentStorage.removeItem(BUFFER_KEY)).catch(() => {});
522
- await bufferWrites;
523
- } catch {
524
- // ignore
525
- }
526
- }
527
-
528
- /**
529
- * Load the persisted preserve-log flag and, if enabled, restore the saved
530
- * buffer. If disabled, drop any saved buffer so the session starts fresh.
531
- * Idempotent — safe to call from ConsoleRoot on every mount.
532
- */
533
- async function initConsolePersistence() {
534
- if (persistenceReady || !(0, _sharedUi.hasAccountAccess)()) return;
535
- persistenceReady = true;
536
- const generation = persistenceGeneration;
537
- try {
538
- preserveLog = (await _sharedUi.persistentStorage.getItem(PRESERVE_KEY)) === "true";
539
- } catch {
540
- preserveLog = false;
541
- }
542
- if (generation !== persistenceGeneration || !(0, _sharedUi.hasAccountAccess)()) {
543
- persistenceReady = false;
544
- return;
545
- }
546
- emitPreserve();
547
- if (!preserveLog) {
548
- await clearSavedBuffer();
549
- return;
550
- }
551
- try {
552
- const raw = await _sharedUi.persistentStorage.getItem(BUFFER_KEY);
553
- if (generation !== persistenceGeneration || !(0, _sharedUi.hasAccountAccess)()) {
554
- persistenceReady = false;
555
- return;
556
- }
557
- if (raw) {
558
- const saved = JSON.parse(raw);
559
- if (Array.isArray(saved) && saved.length) {
560
- // Prepend restored entries before anything captured this session.
561
- store.update(current => [...saved, ...current]);
562
- store.bumpIdAbove(Math.max(...saved.map(e => e.id)));
563
- }
564
- }
565
- } catch {
566
- // Corrupt/absent buffer — ignore.
567
- }
568
- }
569
-
570
- /** Whether logs persist across reload. */
571
- function getPreserveLog() {
572
- return preserveLog;
573
- }
574
-
575
- /** Toggle preserve-log. Persists the flag; saves or drops the buffer. */
576
- function setPreserveLog(next) {
577
- if (preserveLog === next) return;
578
- preserveLog = next;
579
- emitPreserve();
580
- _sharedUi.persistentStorage.setItem(PRESERVE_KEY, String(next)).catch(() => {});
581
- if (next) saveBuffer();else clearSavedBuffer();
582
- }
583
-
584
- /** Subscribe to preserve-log changes. Returns an unsubscribe function. */
585
- function subscribePreserveLog(listener) {
586
- preserveListeners.add(listener);
587
- return () => {
588
- preserveListeners.delete(listener);
589
- };
590
- }
591
- //# sourceMappingURL=consoleLogStore.js.map
7
+ This error is located at:
8
+ ${compactComponentStack(e.componentStack)}`:"";const s=`${t} ${n}${r}`;pushFatal({id:store.allocId(),method:"error",level:"error",args:[t,n,...r?[r.trim()]:[]],message:s,timestamp:Date.now(),stack:formatNativeStack(e),fatal:true},o)}function installNativeExceptionCapture(){const e=globalThis.RN$registerExceptionListener;if(typeof e!=="function")return false;e(t=>{try{if(!captureDisabledByUser)recordNativeException(t)}catch{}});return true}function installFatalErrorCapture(){if(fatalInstalled)return;const e=installNativeExceptionCapture();const t=globalThis.ErrorUtils;if(typeof t?.setGlobalHandler!=="function"){fatalInstalled=e;return}fatalInstalled=true;const n=typeof t.getGlobalHandler==="function"?t.getGlobalHandler():void 0;t.setGlobalHandler((o,r)=>{try{if(!captureDisabledByUser)recordFatalError(o,r)}catch{}if(typeof n==="function")n(o,r)})}function installConsoleCapture(){if((0,_sharedUi.getAccountAccess)().status==="locked")return;captureDisabledByUser=false;installFatalErrorCapture();if(installed)return;installed=true;for(const e of METHODS){const t=console[e];if(typeof t!=="function")continue;originals[e]=t;console[e]=function n(...o){record(e,o);t.apply(console,o)}}}function uninstallConsoleCapture(){captureDisabledByUser=true;if(!installed)return;installed=false;for(const e of METHODS){const t=originals[e];if(t)console[e]=t;delete originals[e]}}function subscribe(e){return store.subscribe(e)}function getEntries(){return store.getSnapshot()}function clearEntries(){void clearSavedBuffer();store.clear()}function replaceEntries(e){store.set(e)}function onClear(e){return store.onClear(e)}const PRESERVE_KEY="@react_buoy_console_preserve";const BUFFER_KEY="@react_buoy_console_buffer";const SAVE_DEBOUNCE_MS=600;let preserveLog=false;let persistenceReady=false;let persistenceGeneration=0;let bufferWrites=Promise.resolve();const preserveListeners=new Set;let saveTimer=null;function emitPreserve(){for(const e of preserveListeners)e()}function scheduleSave(){if(!preserveLog||!(0,_sharedUi.hasAccountAccess)())return;if(saveTimer)clearTimeout(saveTimer);saveTimer=setTimeout(saveBuffer,SAVE_DEBOUNCE_MS)}async function saveBuffer(){saveTimer=null;if(!preserveLog||!(0,_sharedUi.hasAccountAccess)())return;try{const e=store.getSnapshot().map(n=>({...n,args:(0,_sanitize.sanitizeArgs)(n.args)}));const t=persistenceGeneration;bufferWrites=bufferWrites.then(async()=>{if(t!==persistenceGeneration||!preserveLog||!(0,_sharedUi.hasAccountAccess)())return;await _sharedUi.persistentStorage.setItem(BUFFER_KEY,JSON.stringify(e))}).catch(()=>{});await bufferWrites}catch{}}async function clearSavedBuffer(){persistenceGeneration++;persistenceReady=false;try{bufferWrites=bufferWrites.then(()=>_sharedUi.persistentStorage.removeItem(BUFFER_KEY)).catch(()=>{});await bufferWrites}catch{}}async function initConsolePersistence(){if(persistenceReady||!(0,_sharedUi.hasAccountAccess)())return;persistenceReady=true;const e=persistenceGeneration;try{preserveLog=await _sharedUi.persistentStorage.getItem(PRESERVE_KEY)==="true"}catch{preserveLog=false}if(e!==persistenceGeneration||!(0,_sharedUi.hasAccountAccess)()){persistenceReady=false;return}emitPreserve();if(!preserveLog){await clearSavedBuffer();return}try{const t=await _sharedUi.persistentStorage.getItem(BUFFER_KEY);if(e!==persistenceGeneration||!(0,_sharedUi.hasAccountAccess)()){persistenceReady=false;return}if(t){const n=JSON.parse(t);if(Array.isArray(n)&&n.length){store.update(o=>[...n,...o]);store.bumpIdAbove(Math.max(...n.map(o=>o.id)))}}}catch{}}function getPreserveLog(){return preserveLog}function setPreserveLog(e){if(preserveLog===e)return;preserveLog=e;emitPreserve();_sharedUi.persistentStorage.setItem(PRESERVE_KEY,String(e)).catch(()=>{});if(e)saveBuffer();else clearSavedBuffer()}function subscribePreserveLog(e){preserveListeners.add(e);return()=>{preserveListeners.delete(e)}}