@logbrew/react-native 0.1.1 → 0.1.3
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/LogBrewReactNative.podspec +22 -0
- package/README.md +24 -0
- package/android/build.gradle +42 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +636 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +225 -0
- package/android/src/main/java/co/logbrew/reactnative/LogBrewReactNativePackage.java +20 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +39 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +41 -0
- package/fatal-replay.cjs +531 -0
- package/global-errors.cjs +414 -0
- package/global-errors.d.cts +131 -0
- package/global-errors.d.ts +131 -0
- package/global-errors.js +7 -0
- package/global-errors.native.js +39 -0
- package/index.native.d.ts +2 -0
- package/index.native.js +3 -0
- package/ios/LBRNFatalRecordStore.h +25 -0
- package/ios/LBRNFatalRecordStore.m +542 -0
- package/ios/LBRNFatalStoreModule.h +4 -0
- package/ios/LBRNFatalStoreModule.mm +147 -0
- package/package.json +47 -3
- package/react-native.config.js +11 -0
- package/src/NativeLogBrewFatalStore.ts +11 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { createReactNativeErrorEvent } = require("./index.cjs");
|
|
4
|
+
const { createFatalController } = require("./fatal-replay.cjs");
|
|
5
|
+
|
|
6
|
+
const AUTOMATIC_ERROR_MESSAGE = "React Native global JavaScript report";
|
|
7
|
+
const MAX_STACK_BYTES = 16 * 1024;
|
|
8
|
+
const MAX_STACK_FRAMES = 32;
|
|
9
|
+
const SAFE_ERROR_NAMES = new Set([
|
|
10
|
+
"Error",
|
|
11
|
+
"EvalError",
|
|
12
|
+
"RangeError",
|
|
13
|
+
"ReferenceError",
|
|
14
|
+
"SyntaxError",
|
|
15
|
+
"TypeError",
|
|
16
|
+
"URIError"
|
|
17
|
+
]);
|
|
18
|
+
const FATAL_SANITIZERS = Object.freeze({
|
|
19
|
+
errorName: safeErrorName,
|
|
20
|
+
isErrorName: (value) => SAFE_ERROR_NAMES.has(value),
|
|
21
|
+
stackFrame: safeStackFrameParts
|
|
22
|
+
});
|
|
23
|
+
const installations = new WeakMap();
|
|
24
|
+
let nextEventSequence = 0;
|
|
25
|
+
|
|
26
|
+
function installLogBrewReactNativeGlobalErrorHandler({
|
|
27
|
+
client,
|
|
28
|
+
errorUtils = globalThis?.ErrorUtils,
|
|
29
|
+
fatalStore,
|
|
30
|
+
onDiagnostic
|
|
31
|
+
} = {}) {
|
|
32
|
+
const issue = safeFunction(client, "issue");
|
|
33
|
+
const getGlobalHandler = safeFunction(errorUtils, "getGlobalHandler");
|
|
34
|
+
const setGlobalHandler = safeFunction(errorUtils, "setGlobalHandler");
|
|
35
|
+
if (!issue || !getGlobalHandler || !setGlobalHandler || !isObjectLike(errorUtils)) {
|
|
36
|
+
emitDiagnostic(onDiagnostic, "handler_unavailable");
|
|
37
|
+
return inactiveInstallation();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const existing = installations.get(errorUtils);
|
|
41
|
+
if (existing?.health().active) {
|
|
42
|
+
return existing;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let previousHandler;
|
|
46
|
+
try {
|
|
47
|
+
previousHandler = getGlobalHandler.call(errorUtils);
|
|
48
|
+
} catch {
|
|
49
|
+
emitDiagnostic(onDiagnostic, "handler_unavailable");
|
|
50
|
+
return inactiveInstallation();
|
|
51
|
+
}
|
|
52
|
+
if (typeof previousHandler !== "function") {
|
|
53
|
+
emitDiagnostic(onDiagnostic, "handler_unavailable");
|
|
54
|
+
return inactiveInstallation();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const state = {
|
|
58
|
+
active: true,
|
|
59
|
+
capturedEvents: 0,
|
|
60
|
+
handling: false,
|
|
61
|
+
lastOutcome: "idle",
|
|
62
|
+
suppressedEvents: 0
|
|
63
|
+
};
|
|
64
|
+
const fatal = createFatalController({
|
|
65
|
+
client,
|
|
66
|
+
eventMessage: AUTOMATIC_ERROR_MESSAGE,
|
|
67
|
+
fatalStore,
|
|
68
|
+
issue,
|
|
69
|
+
onDiagnostic,
|
|
70
|
+
sanitizers: FATAL_SANITIZERS
|
|
71
|
+
});
|
|
72
|
+
const capturedErrors = new WeakSet();
|
|
73
|
+
const handler = (error, isFatal) => {
|
|
74
|
+
if (!state.active) {
|
|
75
|
+
previousHandler(error, isFatal);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (state.handling) {
|
|
79
|
+
recordSuppression(state, onDiagnostic, "recursive_capture_suppressed");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
state.handling = true;
|
|
83
|
+
try {
|
|
84
|
+
if (isFatal === true) {
|
|
85
|
+
if (fatal.available) {
|
|
86
|
+
state.lastOutcome = fatal.store(error) ? "fatal_stored" : "fatal_store_failed";
|
|
87
|
+
} else {
|
|
88
|
+
state.lastOutcome = "fatal_unsupported";
|
|
89
|
+
emitDiagnostic(onDiagnostic, "fatal_capture_requires_sync_store");
|
|
90
|
+
}
|
|
91
|
+
} else if (isObjectLike(error) && capturedErrors.has(error)) {
|
|
92
|
+
recordSuppression(state, onDiagnostic, "duplicate_capture_suppressed");
|
|
93
|
+
} else {
|
|
94
|
+
captureNonfatalError(client, issue, error, capturedErrors, state, onDiagnostic);
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
try {
|
|
98
|
+
if (typeof previousHandler === "function") {
|
|
99
|
+
previousHandler(error, isFatal);
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
state.handling = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const installation = Object.freeze({
|
|
108
|
+
discardPendingFatalRecord() {
|
|
109
|
+
return fatal.discard();
|
|
110
|
+
},
|
|
111
|
+
fatalHealth() {
|
|
112
|
+
return fatal.health();
|
|
113
|
+
},
|
|
114
|
+
health() {
|
|
115
|
+
return healthSnapshot(state);
|
|
116
|
+
},
|
|
117
|
+
remove() {
|
|
118
|
+
if (!state.active) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
let ownsHandler;
|
|
122
|
+
try {
|
|
123
|
+
ownsHandler = getGlobalHandler.call(errorUtils) === handler;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (!ownsHandler) {
|
|
128
|
+
state.active = false;
|
|
129
|
+
state.lastOutcome = "handler_replaced";
|
|
130
|
+
installations.delete(errorUtils);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
setGlobalHandler.call(errorUtils, previousHandler);
|
|
135
|
+
} catch {
|
|
136
|
+
state.lastOutcome = "remove_failed";
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
state.active = false;
|
|
140
|
+
state.lastOutcome = "removed";
|
|
141
|
+
installations.delete(errorUtils);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
setGlobalHandler.call(errorUtils, handler);
|
|
148
|
+
} catch {
|
|
149
|
+
state.active = false;
|
|
150
|
+
try {
|
|
151
|
+
if (getGlobalHandler.call(errorUtils) === handler) {
|
|
152
|
+
setGlobalHandler.call(errorUtils, previousHandler);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// An inactive wrapper only delegates if a platform setter failed after mutation.
|
|
156
|
+
}
|
|
157
|
+
emitDiagnostic(onDiagnostic, "handler_unavailable");
|
|
158
|
+
return inactiveInstallation();
|
|
159
|
+
}
|
|
160
|
+
installations.set(errorUtils, installation);
|
|
161
|
+
fatal.replay();
|
|
162
|
+
return installation;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function captureNonfatalError(client, issue, error, capturedErrors, state, onDiagnostic) {
|
|
166
|
+
const tracksIdentity = isObjectLike(error);
|
|
167
|
+
if (tracksIdentity) {
|
|
168
|
+
capturedErrors.add(error);
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const event = createAutomaticErrorEvent(error);
|
|
172
|
+
issue.call(client, event.id, event.timestamp, event.attributes);
|
|
173
|
+
state.capturedEvents = incrementBounded(state.capturedEvents);
|
|
174
|
+
state.lastOutcome = "captured";
|
|
175
|
+
} catch {
|
|
176
|
+
if (tracksIdentity) {
|
|
177
|
+
capturedErrors.delete(error);
|
|
178
|
+
}
|
|
179
|
+
state.lastOutcome = "capture_failed";
|
|
180
|
+
emitDiagnostic(onDiagnostic, "capture_failed");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createAutomaticErrorEvent(error) {
|
|
185
|
+
const sanitizedError = new Error(AUTOMATIC_ERROR_MESSAGE);
|
|
186
|
+
sanitizedError.name = safeErrorName(error);
|
|
187
|
+
const stack = safeStack(error);
|
|
188
|
+
if (stack) {
|
|
189
|
+
sanitizedError.stack = stack;
|
|
190
|
+
} else {
|
|
191
|
+
delete sanitizedError.stack;
|
|
192
|
+
}
|
|
193
|
+
const event = createReactNativeErrorEvent(sanitizedError, {
|
|
194
|
+
id: nextEventId(),
|
|
195
|
+
includeStack: false
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
...event,
|
|
199
|
+
attributes: {
|
|
200
|
+
...event.attributes,
|
|
201
|
+
title: AUTOMATIC_ERROR_MESSAGE,
|
|
202
|
+
message: AUTOMATIC_ERROR_MESSAGE,
|
|
203
|
+
metadata: {
|
|
204
|
+
...event.attributes.metadata,
|
|
205
|
+
automatic: true,
|
|
206
|
+
fatal: false,
|
|
207
|
+
handled: true,
|
|
208
|
+
mechanism: "react_native_error_utils",
|
|
209
|
+
source: "react-native.global_error"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function safeStack(error) {
|
|
216
|
+
const stack = safeReadProperty(error, "stack");
|
|
217
|
+
const candidate = typeof stack === "string"
|
|
218
|
+
? stack.slice(0, MAX_STACK_BYTES)
|
|
219
|
+
: "";
|
|
220
|
+
const frames = [];
|
|
221
|
+
for (const line of candidate.split(/\r?\n/u)) {
|
|
222
|
+
const frame = safeStackFrame(line);
|
|
223
|
+
if (frame) {
|
|
224
|
+
frames.push(` at frame (${frame})`);
|
|
225
|
+
if (frames.length === MAX_STACK_FRAMES) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return frames.length === 0
|
|
231
|
+
? undefined
|
|
232
|
+
: `${safeErrorName(error)}: ${AUTOMATIC_ERROR_MESSAGE}\n${frames.join("\n")}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function safeStackFrame(line) {
|
|
236
|
+
const frame = safeStackFrameParts(line);
|
|
237
|
+
return frame
|
|
238
|
+
? `${frame.filename}:${frame.line}:${frame.column}`
|
|
239
|
+
: undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function safeStackFrameParts(line) {
|
|
243
|
+
let location = typeof line === "string" ? line.trim() : "";
|
|
244
|
+
if (!location) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
if (location.startsWith("at ")) {
|
|
248
|
+
location = location.slice(3).trim();
|
|
249
|
+
if (location.endsWith(")") && location.includes("(")) {
|
|
250
|
+
location = location.slice(location.lastIndexOf("(") + 1, -1);
|
|
251
|
+
}
|
|
252
|
+
} else if (location.includes("@")) {
|
|
253
|
+
location = location.slice(location.lastIndexOf("@") + 1);
|
|
254
|
+
}
|
|
255
|
+
const parts = location.split(":");
|
|
256
|
+
if (parts.length < 3) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
const column = positiveInteger(parts.pop());
|
|
260
|
+
const lineNumber = positiveInteger(parts.pop());
|
|
261
|
+
const filename = safeStackFilename(parts.join(":"));
|
|
262
|
+
if (!filename || lineNumber === undefined || column === undefined) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
column,
|
|
267
|
+
filename,
|
|
268
|
+
line: lineNumber
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function safeStackFilename(value) {
|
|
273
|
+
let filename = String(value ?? "").trim().replace(/\\/gu, "/");
|
|
274
|
+
if (!filename || filename.length > 2048 || hasControlCharacter(filename)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
if (/^[a-z][a-z0-9+.-]*:/iu.test(filename)) {
|
|
279
|
+
const parsed = new URL(filename);
|
|
280
|
+
if (!["app:", "http:", "https:"].includes(parsed.protocol)
|
|
281
|
+
|| parsed.username
|
|
282
|
+
|| urlAuthority(filename).includes("@")) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
filename = parsed.pathname;
|
|
286
|
+
} else {
|
|
287
|
+
filename = filename.split(/[?#]/u, 1)[0];
|
|
288
|
+
if (filename.startsWith("/") || /^[A-Za-z]:\//u.test(filename)) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
return filename && filename.length <= 2048 && !hasControlCharacter(filename)
|
|
296
|
+
? filename
|
|
297
|
+
: undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function urlAuthority(value) {
|
|
301
|
+
const schemeMarker = value.indexOf("://");
|
|
302
|
+
if (schemeMarker < 0) {
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
const start = schemeMarker + 3;
|
|
306
|
+
const end = value.indexOf("/", start);
|
|
307
|
+
return value.slice(start, end < 0 ? value.length : end);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function safeErrorName(error) {
|
|
311
|
+
const name = safeReadProperty(error, "name");
|
|
312
|
+
const candidate = typeof name === "string" ? name : "Error";
|
|
313
|
+
return SAFE_ERROR_NAMES.has(candidate) ? candidate : "Error";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function safeReadProperty(value, property) {
|
|
317
|
+
if (!isObjectLike(value)) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return value[property];
|
|
322
|
+
} catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function safeFunction(value, property) {
|
|
328
|
+
const candidate = safeReadProperty(value, property);
|
|
329
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hasControlCharacter(value) {
|
|
333
|
+
return Array.from(value).some((character) => {
|
|
334
|
+
const code = character.codePointAt(0);
|
|
335
|
+
return code !== undefined && (code <= 31 || code === 127);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function positiveInteger(value) {
|
|
340
|
+
if (!/^[1-9][0-9]*$/u.test(String(value))) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const number = Number(value);
|
|
344
|
+
return Number.isSafeInteger(number) && number <= 2147483647 ? number : undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function nextEventId() {
|
|
348
|
+
nextEventSequence = incrementBounded(nextEventSequence);
|
|
349
|
+
return `evt_rn_global_${Date.now().toString(36)}_${nextEventSequence.toString(36)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function incrementBounded(value) {
|
|
353
|
+
return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function recordSuppression(state, onDiagnostic, code) {
|
|
357
|
+
state.suppressedEvents = incrementBounded(state.suppressedEvents);
|
|
358
|
+
state.lastOutcome = code === "recursive_capture_suppressed" ? "recursive_suppressed" : "duplicate_suppressed";
|
|
359
|
+
emitDiagnostic(onDiagnostic, code);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function emitDiagnostic(onDiagnostic, code) {
|
|
363
|
+
if (typeof onDiagnostic !== "function") {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
onDiagnostic(Object.freeze({ code }));
|
|
368
|
+
} catch {
|
|
369
|
+
// Diagnostics must never interfere with application error handling.
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function inactiveInstallation() {
|
|
374
|
+
const snapshot = Object.freeze({
|
|
375
|
+
active: false,
|
|
376
|
+
capturedEvents: 0,
|
|
377
|
+
lastOutcome: "unavailable",
|
|
378
|
+
suppressedEvents: 0
|
|
379
|
+
});
|
|
380
|
+
const fatalSnapshot = Object.freeze({
|
|
381
|
+
acknowledgedRecords: 0,
|
|
382
|
+
available: false,
|
|
383
|
+
corruptRecords: 0,
|
|
384
|
+
droppedRecords: 0,
|
|
385
|
+
lastOutcome: "unavailable",
|
|
386
|
+
replayedRecords: 0,
|
|
387
|
+
storedRecords: 0
|
|
388
|
+
});
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
discardPendingFatalRecord: () => false,
|
|
391
|
+
fatalHealth: () => fatalSnapshot,
|
|
392
|
+
health: () => snapshot,
|
|
393
|
+
remove: () => false
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function healthSnapshot(state) {
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
active: state.active,
|
|
400
|
+
capturedEvents: state.capturedEvents,
|
|
401
|
+
lastOutcome: state.lastOutcome,
|
|
402
|
+
suppressedEvents: state.suppressedEvents
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isObjectLike(value) {
|
|
407
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const defaultExport = {
|
|
411
|
+
installLogBrewReactNativeGlobalErrorHandler
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
module.exports = { ...defaultExport, default: defaultExport };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { LogBrewClient } from "@logbrew/sdk";
|
|
2
|
+
|
|
3
|
+
export type ReactNativeGlobalErrorHandler = (error: unknown, isFatal?: boolean) => void;
|
|
4
|
+
|
|
5
|
+
export type ReactNativeErrorUtilsLike = {
|
|
6
|
+
getGlobalHandler(): ReactNativeGlobalErrorHandler | undefined;
|
|
7
|
+
setGlobalHandler(handler: ReactNativeGlobalErrorHandler | undefined): void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ReactNativeGlobalErrorDiagnosticCode =
|
|
11
|
+
| "capture_failed"
|
|
12
|
+
| "duplicate_capture_suppressed"
|
|
13
|
+
| "fatal_acknowledge_failed"
|
|
14
|
+
| "fatal_capture_requires_sync_store"
|
|
15
|
+
| "fatal_corrupt_record_discarded"
|
|
16
|
+
| "fatal_discard_failed"
|
|
17
|
+
| "fatal_record_dropped"
|
|
18
|
+
| "fatal_replay_failed"
|
|
19
|
+
| "fatal_replay_not_admitted"
|
|
20
|
+
| "fatal_store_failed"
|
|
21
|
+
| "handler_unavailable"
|
|
22
|
+
| "recursive_capture_suppressed";
|
|
23
|
+
|
|
24
|
+
export type ReactNativeGlobalErrorDiagnostic = Readonly<{
|
|
25
|
+
code: ReactNativeGlobalErrorDiagnosticCode;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export type ReactNativeGlobalErrorHealth = Readonly<{
|
|
29
|
+
active: boolean;
|
|
30
|
+
capturedEvents: number;
|
|
31
|
+
lastOutcome:
|
|
32
|
+
| "capture_failed"
|
|
33
|
+
| "captured"
|
|
34
|
+
| "duplicate_suppressed"
|
|
35
|
+
| "fatal_stored"
|
|
36
|
+
| "fatal_store_failed"
|
|
37
|
+
| "fatal_unsupported"
|
|
38
|
+
| "handler_replaced"
|
|
39
|
+
| "idle"
|
|
40
|
+
| "recursive_suppressed"
|
|
41
|
+
| "remove_failed"
|
|
42
|
+
| "removed"
|
|
43
|
+
| "unavailable";
|
|
44
|
+
suppressedEvents: number;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export type ReactNativeFatalRecord = Readonly<{
|
|
48
|
+
corruptRecords: number;
|
|
49
|
+
droppedRecords: number;
|
|
50
|
+
errorName: string;
|
|
51
|
+
id: string;
|
|
52
|
+
schemaVersion: 1;
|
|
53
|
+
stackFrames: ReadonlyArray<Readonly<{
|
|
54
|
+
column: number;
|
|
55
|
+
filename: string;
|
|
56
|
+
line: number;
|
|
57
|
+
}>>;
|
|
58
|
+
timestamp: string;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
export type ReactNativeFatalStoreResult = Readonly<{
|
|
62
|
+
corruptRecords?: number;
|
|
63
|
+
droppedRecords?: number;
|
|
64
|
+
record?: ReactNativeFatalRecord;
|
|
65
|
+
recordId?: string;
|
|
66
|
+
status: string;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
export type ReactNativeFatalStoreLike = {
|
|
70
|
+
writeFatalRecord(record: ReactNativeFatalRecord): ReactNativeFatalStoreResult;
|
|
71
|
+
readFatalRecord(): ReactNativeFatalStoreResult;
|
|
72
|
+
acknowledgeFatalRecord(recordId: string): ReactNativeFatalStoreResult;
|
|
73
|
+
discardFatalRecord(): ReactNativeFatalStoreResult;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ReactNativeFatalReplayHealth = Readonly<{
|
|
77
|
+
acknowledgedRecords: number;
|
|
78
|
+
available: boolean;
|
|
79
|
+
corruptRecords: number;
|
|
80
|
+
droppedRecords: number;
|
|
81
|
+
lastOutcome:
|
|
82
|
+
| "acknowledge_failed"
|
|
83
|
+
| "acknowledged"
|
|
84
|
+
| "corrupt_discarded"
|
|
85
|
+
| "discard_failed"
|
|
86
|
+
| "discarded"
|
|
87
|
+
| "dropped_pending"
|
|
88
|
+
| "empty"
|
|
89
|
+
| "idle"
|
|
90
|
+
| "replay_failed"
|
|
91
|
+
| "replay_not_admitted"
|
|
92
|
+
| "storage_error"
|
|
93
|
+
| "stored"
|
|
94
|
+
| "stored_after_corruption"
|
|
95
|
+
| "unavailable";
|
|
96
|
+
replayedRecords: number;
|
|
97
|
+
storedRecords: number;
|
|
98
|
+
}>;
|
|
99
|
+
|
|
100
|
+
export type InstallLogBrewReactNativeGlobalErrorHandlerOptions = {
|
|
101
|
+
client: Pick<LogBrewClient, "issue">
|
|
102
|
+
& Partial<Pick<LogBrewClient, "droppedEvents" | "pendingEvents">>;
|
|
103
|
+
errorUtils?: ReactNativeErrorUtilsLike;
|
|
104
|
+
fatalStore?: ReactNativeFatalStoreLike;
|
|
105
|
+
onDiagnostic?: (diagnostic: ReactNativeGlobalErrorDiagnostic) => void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type LogBrewReactNativeGlobalErrorHandlerInstallation = Readonly<{
|
|
109
|
+
discardPendingFatalRecord(): boolean;
|
|
110
|
+
fatalHealth(): ReactNativeFatalReplayHealth;
|
|
111
|
+
health(): ReactNativeGlobalErrorHealth;
|
|
112
|
+
remove(): boolean;
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Install reversible automatic capture for React Native global JavaScript errors.
|
|
117
|
+
*
|
|
118
|
+
* The React Native conditional export injects its synchronous native fatal store by default.
|
|
119
|
+
* Direct Node ESM/CJS callers can inject a compatible store explicitly. Fatal replay is
|
|
120
|
+
* stable-ID at-least-once and acknowledges only after observable local queue admission.
|
|
121
|
+
* This helper does not claim local exactly-once delivery or install Promise rejection handling.
|
|
122
|
+
*/
|
|
123
|
+
export declare function installLogBrewReactNativeGlobalErrorHandler(
|
|
124
|
+
options: InstallLogBrewReactNativeGlobalErrorHandlerOptions
|
|
125
|
+
): LogBrewReactNativeGlobalErrorHandlerInstallation;
|
|
126
|
+
|
|
127
|
+
declare const logBrewReactNativeGlobalErrors: {
|
|
128
|
+
installLogBrewReactNativeGlobalErrorHandler: typeof installLogBrewReactNativeGlobalErrorHandler;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default logBrewReactNativeGlobalErrors;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { LogBrewClient } from "@logbrew/sdk";
|
|
2
|
+
|
|
3
|
+
export type ReactNativeGlobalErrorHandler = (error: unknown, isFatal?: boolean) => void;
|
|
4
|
+
|
|
5
|
+
export type ReactNativeErrorUtilsLike = {
|
|
6
|
+
getGlobalHandler(): ReactNativeGlobalErrorHandler | undefined;
|
|
7
|
+
setGlobalHandler(handler: ReactNativeGlobalErrorHandler | undefined): void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ReactNativeGlobalErrorDiagnosticCode =
|
|
11
|
+
| "capture_failed"
|
|
12
|
+
| "duplicate_capture_suppressed"
|
|
13
|
+
| "fatal_acknowledge_failed"
|
|
14
|
+
| "fatal_capture_requires_sync_store"
|
|
15
|
+
| "fatal_corrupt_record_discarded"
|
|
16
|
+
| "fatal_discard_failed"
|
|
17
|
+
| "fatal_record_dropped"
|
|
18
|
+
| "fatal_replay_failed"
|
|
19
|
+
| "fatal_replay_not_admitted"
|
|
20
|
+
| "fatal_store_failed"
|
|
21
|
+
| "handler_unavailable"
|
|
22
|
+
| "recursive_capture_suppressed";
|
|
23
|
+
|
|
24
|
+
export type ReactNativeGlobalErrorDiagnostic = Readonly<{
|
|
25
|
+
code: ReactNativeGlobalErrorDiagnosticCode;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export type ReactNativeGlobalErrorHealth = Readonly<{
|
|
29
|
+
active: boolean;
|
|
30
|
+
capturedEvents: number;
|
|
31
|
+
lastOutcome:
|
|
32
|
+
| "capture_failed"
|
|
33
|
+
| "captured"
|
|
34
|
+
| "duplicate_suppressed"
|
|
35
|
+
| "fatal_stored"
|
|
36
|
+
| "fatal_store_failed"
|
|
37
|
+
| "fatal_unsupported"
|
|
38
|
+
| "handler_replaced"
|
|
39
|
+
| "idle"
|
|
40
|
+
| "recursive_suppressed"
|
|
41
|
+
| "remove_failed"
|
|
42
|
+
| "removed"
|
|
43
|
+
| "unavailable";
|
|
44
|
+
suppressedEvents: number;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export type ReactNativeFatalRecord = Readonly<{
|
|
48
|
+
corruptRecords: number;
|
|
49
|
+
droppedRecords: number;
|
|
50
|
+
errorName: string;
|
|
51
|
+
id: string;
|
|
52
|
+
schemaVersion: 1;
|
|
53
|
+
stackFrames: ReadonlyArray<Readonly<{
|
|
54
|
+
column: number;
|
|
55
|
+
filename: string;
|
|
56
|
+
line: number;
|
|
57
|
+
}>>;
|
|
58
|
+
timestamp: string;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
export type ReactNativeFatalStoreResult = Readonly<{
|
|
62
|
+
corruptRecords?: number;
|
|
63
|
+
droppedRecords?: number;
|
|
64
|
+
record?: ReactNativeFatalRecord;
|
|
65
|
+
recordId?: string;
|
|
66
|
+
status: string;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
export type ReactNativeFatalStoreLike = {
|
|
70
|
+
writeFatalRecord(record: ReactNativeFatalRecord): ReactNativeFatalStoreResult;
|
|
71
|
+
readFatalRecord(): ReactNativeFatalStoreResult;
|
|
72
|
+
acknowledgeFatalRecord(recordId: string): ReactNativeFatalStoreResult;
|
|
73
|
+
discardFatalRecord(): ReactNativeFatalStoreResult;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ReactNativeFatalReplayHealth = Readonly<{
|
|
77
|
+
acknowledgedRecords: number;
|
|
78
|
+
available: boolean;
|
|
79
|
+
corruptRecords: number;
|
|
80
|
+
droppedRecords: number;
|
|
81
|
+
lastOutcome:
|
|
82
|
+
| "acknowledge_failed"
|
|
83
|
+
| "acknowledged"
|
|
84
|
+
| "corrupt_discarded"
|
|
85
|
+
| "discard_failed"
|
|
86
|
+
| "discarded"
|
|
87
|
+
| "dropped_pending"
|
|
88
|
+
| "empty"
|
|
89
|
+
| "idle"
|
|
90
|
+
| "replay_failed"
|
|
91
|
+
| "replay_not_admitted"
|
|
92
|
+
| "storage_error"
|
|
93
|
+
| "stored"
|
|
94
|
+
| "stored_after_corruption"
|
|
95
|
+
| "unavailable";
|
|
96
|
+
replayedRecords: number;
|
|
97
|
+
storedRecords: number;
|
|
98
|
+
}>;
|
|
99
|
+
|
|
100
|
+
export type InstallLogBrewReactNativeGlobalErrorHandlerOptions = {
|
|
101
|
+
client: Pick<LogBrewClient, "issue">
|
|
102
|
+
& Partial<Pick<LogBrewClient, "droppedEvents" | "pendingEvents">>;
|
|
103
|
+
errorUtils?: ReactNativeErrorUtilsLike;
|
|
104
|
+
fatalStore?: ReactNativeFatalStoreLike;
|
|
105
|
+
onDiagnostic?: (diagnostic: ReactNativeGlobalErrorDiagnostic) => void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type LogBrewReactNativeGlobalErrorHandlerInstallation = Readonly<{
|
|
109
|
+
discardPendingFatalRecord(): boolean;
|
|
110
|
+
fatalHealth(): ReactNativeFatalReplayHealth;
|
|
111
|
+
health(): ReactNativeGlobalErrorHealth;
|
|
112
|
+
remove(): boolean;
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Install reversible automatic capture for React Native global JavaScript errors.
|
|
117
|
+
*
|
|
118
|
+
* The React Native conditional export injects its synchronous native fatal store by default.
|
|
119
|
+
* Direct Node ESM/CJS callers can inject a compatible store explicitly. Fatal replay is
|
|
120
|
+
* stable-ID at-least-once and acknowledges only after observable local queue admission.
|
|
121
|
+
* This helper does not claim local exactly-once delivery or install Promise rejection handling.
|
|
122
|
+
*/
|
|
123
|
+
export declare function installLogBrewReactNativeGlobalErrorHandler(
|
|
124
|
+
options: InstallLogBrewReactNativeGlobalErrorHandlerOptions
|
|
125
|
+
): LogBrewReactNativeGlobalErrorHandlerInstallation;
|
|
126
|
+
|
|
127
|
+
declare const logBrewReactNativeGlobalErrors: {
|
|
128
|
+
installLogBrewReactNativeGlobalErrorHandler: typeof installLogBrewReactNativeGlobalErrorHandler;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default logBrewReactNativeGlobalErrors;
|