@logbrew/react-native 0.1.2 → 0.1.4
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 +3 -3
- 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 +51 -3
- package/global-errors.d.cts +72 -4
- package/global-errors.d.ts +72 -4
- 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 +33 -4
- package/react-native.config.js +11 -0
- package/src/NativeLogBrewFatalStore.ts +11 -0
package/fatal-replay.cjs
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const FATAL_RECORD_SCHEMA_VERSION = 1;
|
|
4
|
+
const MAX_ADMITTED_FATAL_IDS = 256;
|
|
5
|
+
const MAX_FATAL_FILENAME_BYTES = 512;
|
|
6
|
+
const MAX_FATAL_STACK_BYTES = 16 * 1024;
|
|
7
|
+
const MAX_FATAL_STACK_FRAMES = 24;
|
|
8
|
+
|
|
9
|
+
const admittedFatalIdsByClient = new WeakMap();
|
|
10
|
+
let nextFatalSequence = 0;
|
|
11
|
+
|
|
12
|
+
function createFatalController({
|
|
13
|
+
client,
|
|
14
|
+
eventMessage,
|
|
15
|
+
fatalStore,
|
|
16
|
+
issue,
|
|
17
|
+
onDiagnostic,
|
|
18
|
+
sanitizers
|
|
19
|
+
}) {
|
|
20
|
+
const methods = fatalStoreMethods(fatalStore);
|
|
21
|
+
const state = {
|
|
22
|
+
acknowledgedRecords: 0,
|
|
23
|
+
available: methods !== undefined,
|
|
24
|
+
corruptRecords: 0,
|
|
25
|
+
droppedRecords: 0,
|
|
26
|
+
lastOutcome: methods ? "idle" : "unavailable",
|
|
27
|
+
replayedRecords: 0,
|
|
28
|
+
storedRecords: 0
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
available: state.available,
|
|
33
|
+
discard() {
|
|
34
|
+
return discardPendingFatal(fatalStore, methods, state, onDiagnostic);
|
|
35
|
+
},
|
|
36
|
+
health() {
|
|
37
|
+
return fatalHealthSnapshot(state);
|
|
38
|
+
},
|
|
39
|
+
replay() {
|
|
40
|
+
replayPendingFatal({
|
|
41
|
+
client,
|
|
42
|
+
eventMessage,
|
|
43
|
+
fatalStore,
|
|
44
|
+
issue,
|
|
45
|
+
methods,
|
|
46
|
+
onDiagnostic,
|
|
47
|
+
sanitizers,
|
|
48
|
+
state
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
store(error) {
|
|
52
|
+
return storeFatalError({
|
|
53
|
+
error,
|
|
54
|
+
fatalStore,
|
|
55
|
+
methods,
|
|
56
|
+
onDiagnostic,
|
|
57
|
+
sanitizers,
|
|
58
|
+
state
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fatalStoreMethods(fatalStore) {
|
|
65
|
+
if (!isObjectLike(fatalStore)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const methods = {
|
|
69
|
+
acknowledge: safeFunction(fatalStore, "acknowledgeFatalRecord"),
|
|
70
|
+
discard: safeFunction(fatalStore, "discardFatalRecord"),
|
|
71
|
+
read: safeFunction(fatalStore, "readFatalRecord"),
|
|
72
|
+
write: safeFunction(fatalStore, "writeFatalRecord")
|
|
73
|
+
};
|
|
74
|
+
return Object.values(methods).every((method) => typeof method === "function")
|
|
75
|
+
? methods
|
|
76
|
+
: undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function discardPendingFatal(fatalStore, methods, state, onDiagnostic) {
|
|
80
|
+
if (!methods) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = methods.discard.call(fatalStore);
|
|
86
|
+
} catch {
|
|
87
|
+
state.lastOutcome = "discard_failed";
|
|
88
|
+
emitDiagnostic(onDiagnostic, "fatal_discard_failed");
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (resultStatus(result) !== "discarded") {
|
|
92
|
+
state.lastOutcome = resultStatus(result) === "empty" ? "empty" : "discard_failed";
|
|
93
|
+
if (state.lastOutcome === "discard_failed") {
|
|
94
|
+
emitDiagnostic(onDiagnostic, "fatal_discard_failed");
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
state.lastOutcome = "discarded";
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function storeFatalError({
|
|
103
|
+
error,
|
|
104
|
+
fatalStore,
|
|
105
|
+
methods,
|
|
106
|
+
onDiagnostic,
|
|
107
|
+
sanitizers,
|
|
108
|
+
state
|
|
109
|
+
}) {
|
|
110
|
+
if (!methods) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = methods.write.call(fatalStore, createFatalRecord(error, sanitizers));
|
|
116
|
+
} catch {
|
|
117
|
+
result = undefined;
|
|
118
|
+
}
|
|
119
|
+
const status = resultStatus(result);
|
|
120
|
+
if (status === "stored" || status === "stored_after_corruption") {
|
|
121
|
+
state.storedRecords = incrementBounded(state.storedRecords);
|
|
122
|
+
state.corruptRecords = boundedResultCounter(result, "corruptRecords", state.corruptRecords);
|
|
123
|
+
state.lastOutcome = status;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (status === "dropped_pending") {
|
|
127
|
+
state.droppedRecords = boundedResultCounter(
|
|
128
|
+
result,
|
|
129
|
+
"droppedRecords",
|
|
130
|
+
incrementBounded(state.droppedRecords)
|
|
131
|
+
);
|
|
132
|
+
state.lastOutcome = status;
|
|
133
|
+
emitDiagnostic(onDiagnostic, "fatal_record_dropped");
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
state.lastOutcome = "storage_error";
|
|
137
|
+
emitDiagnostic(onDiagnostic, "fatal_store_failed");
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function replayPendingFatal({
|
|
142
|
+
client,
|
|
143
|
+
eventMessage,
|
|
144
|
+
fatalStore,
|
|
145
|
+
issue,
|
|
146
|
+
methods,
|
|
147
|
+
onDiagnostic,
|
|
148
|
+
sanitizers,
|
|
149
|
+
state
|
|
150
|
+
}) {
|
|
151
|
+
if (!methods) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let result;
|
|
155
|
+
try {
|
|
156
|
+
result = methods.read.call(fatalStore);
|
|
157
|
+
} catch {
|
|
158
|
+
result = undefined;
|
|
159
|
+
}
|
|
160
|
+
const status = resultStatus(result);
|
|
161
|
+
if (status === "empty") {
|
|
162
|
+
state.lastOutcome = "empty";
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (status === "corrupt_discarded") {
|
|
166
|
+
state.corruptRecords = boundedResultCounter(
|
|
167
|
+
result,
|
|
168
|
+
"corruptRecords",
|
|
169
|
+
incrementBounded(state.corruptRecords)
|
|
170
|
+
);
|
|
171
|
+
state.lastOutcome = status;
|
|
172
|
+
emitDiagnostic(onDiagnostic, "fatal_corrupt_record_discarded");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const record = status === "pending"
|
|
176
|
+
? validatedFatalRecord(safeReadProperty(result, "record"), sanitizers)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (!record) {
|
|
179
|
+
state.lastOutcome = "replay_failed";
|
|
180
|
+
emitDiagnostic(onDiagnostic, "fatal_replay_failed");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
state.corruptRecords = Math.max(state.corruptRecords, record.corruptRecords);
|
|
185
|
+
state.droppedRecords = Math.max(state.droppedRecords, record.droppedRecords);
|
|
186
|
+
if (wasAdmittedInRuntime(client, record.id)) {
|
|
187
|
+
acknowledgeFatalRecord(fatalStore, methods, record.id, state, onDiagnostic);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const before = admissionSnapshot(client);
|
|
192
|
+
try {
|
|
193
|
+
issue.call(client, record.id, record.timestamp, fatalEventAttributes(record, eventMessage));
|
|
194
|
+
} catch {
|
|
195
|
+
state.lastOutcome = "replay_failed";
|
|
196
|
+
emitDiagnostic(onDiagnostic, "fatal_replay_failed");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const after = admissionSnapshot(client);
|
|
200
|
+
if (!wasAdmitted(before, after)) {
|
|
201
|
+
state.lastOutcome = "replay_not_admitted";
|
|
202
|
+
emitDiagnostic(onDiagnostic, "fatal_replay_not_admitted");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
rememberAdmittedFatalId(client, record.id);
|
|
207
|
+
state.replayedRecords = incrementBounded(state.replayedRecords);
|
|
208
|
+
acknowledgeFatalRecord(fatalStore, methods, record.id, state, onDiagnostic);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function acknowledgeFatalRecord(fatalStore, methods, recordId, state, onDiagnostic) {
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = methods.acknowledge.call(fatalStore, recordId);
|
|
215
|
+
} catch {
|
|
216
|
+
result = undefined;
|
|
217
|
+
}
|
|
218
|
+
if (resultStatus(result) === "acknowledged"
|
|
219
|
+
&& safeReadProperty(result, "recordId") === recordId) {
|
|
220
|
+
state.acknowledgedRecords = incrementBounded(state.acknowledgedRecords);
|
|
221
|
+
state.lastOutcome = "acknowledged";
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
state.lastOutcome = "acknowledge_failed";
|
|
225
|
+
emitDiagnostic(onDiagnostic, "fatal_acknowledge_failed");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function admissionSnapshot(client) {
|
|
229
|
+
const pendingEvents = safeFunction(client, "pendingEvents");
|
|
230
|
+
const droppedEvents = safeFunction(client, "droppedEvents");
|
|
231
|
+
if (!pendingEvents || !droppedEvents) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const pending = pendingEvents.call(client);
|
|
236
|
+
const dropped = droppedEvents.call(client);
|
|
237
|
+
return isBoundedCounter(pending) && isBoundedCounter(dropped)
|
|
238
|
+
? { dropped, pending }
|
|
239
|
+
: undefined;
|
|
240
|
+
} catch {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function wasAdmitted(before, after) {
|
|
246
|
+
return before !== undefined
|
|
247
|
+
&& after !== undefined
|
|
248
|
+
&& after.pending === before.pending + 1
|
|
249
|
+
&& after.dropped === before.dropped;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function wasAdmittedInRuntime(client, recordId) {
|
|
253
|
+
return isObjectLike(client) && admittedFatalIdsByClient.get(client)?.has(recordId) === true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function rememberAdmittedFatalId(client, recordId) {
|
|
257
|
+
if (!isObjectLike(client)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let admittedIds = admittedFatalIdsByClient.get(client);
|
|
261
|
+
if (!admittedIds) {
|
|
262
|
+
admittedIds = new Set();
|
|
263
|
+
admittedFatalIdsByClient.set(client, admittedIds);
|
|
264
|
+
}
|
|
265
|
+
if (admittedIds.has(recordId)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
admittedIds.add(recordId);
|
|
269
|
+
if (admittedIds.size > MAX_ADMITTED_FATAL_IDS) {
|
|
270
|
+
admittedIds.delete(admittedIds.values().next().value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createFatalRecord(error, sanitizers) {
|
|
275
|
+
return Object.freeze({
|
|
276
|
+
corruptRecords: 0,
|
|
277
|
+
droppedRecords: 0,
|
|
278
|
+
errorName: sanitizers.errorName(error),
|
|
279
|
+
id: nextFatalEventId(),
|
|
280
|
+
schemaVersion: FATAL_RECORD_SCHEMA_VERSION,
|
|
281
|
+
stackFrames: fatalStackFrames(error, sanitizers.stackFrame),
|
|
282
|
+
timestamp: new Date().toISOString()
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fatalStackFrames(error, stackFrame) {
|
|
287
|
+
const stack = safeReadProperty(error, "stack");
|
|
288
|
+
const candidate = typeof stack === "string"
|
|
289
|
+
? stack.slice(0, MAX_FATAL_STACK_BYTES)
|
|
290
|
+
: "";
|
|
291
|
+
const frames = [];
|
|
292
|
+
for (const line of candidate.split(/\r?\n/u)) {
|
|
293
|
+
const frame = stackFrame(line);
|
|
294
|
+
const filename = frame ? normalizedStoredFatalFilename(frame.filename) : undefined;
|
|
295
|
+
if (filename && validFatalFilename(filename)) {
|
|
296
|
+
frames.push(Object.freeze({ ...frame, filename }));
|
|
297
|
+
if (frames.length === MAX_FATAL_STACK_FRAMES) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze(frames);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function fatalEventAttributes(record, eventMessage) {
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
level: "fatal",
|
|
308
|
+
message: eventMessage,
|
|
309
|
+
metadata: Object.freeze({
|
|
310
|
+
automatic: true,
|
|
311
|
+
fatal: true,
|
|
312
|
+
handled: false,
|
|
313
|
+
mechanism: "react_native_error_utils",
|
|
314
|
+
nativeCorruptRecords: record.corruptRecords,
|
|
315
|
+
nativeDroppedRecords: record.droppedRecords,
|
|
316
|
+
replayed: true,
|
|
317
|
+
source: "react-native.global_error"
|
|
318
|
+
}),
|
|
319
|
+
stackFrames: record.stackFrames.map((frame) => Object.freeze({ ...frame })),
|
|
320
|
+
title: eventMessage
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validatedFatalRecord(value, sanitizers) {
|
|
325
|
+
try {
|
|
326
|
+
if (!isPlainObject(value)
|
|
327
|
+
|| !hasExactKeys(value, [
|
|
328
|
+
"corruptRecords",
|
|
329
|
+
"droppedRecords",
|
|
330
|
+
"errorName",
|
|
331
|
+
"id",
|
|
332
|
+
"schemaVersion",
|
|
333
|
+
"stackFrames",
|
|
334
|
+
"timestamp"
|
|
335
|
+
])
|
|
336
|
+
|| value.schemaVersion !== FATAL_RECORD_SCHEMA_VERSION
|
|
337
|
+
|| typeof value.id !== "string"
|
|
338
|
+
|| !/^evt_rn_fatal_[a-z0-9]+(?:_[a-z0-9]+)*$/u.test(value.id)
|
|
339
|
+
|| !sanitizers.isErrorName(value.errorName)
|
|
340
|
+
|| !isBoundedCounter(value.corruptRecords)
|
|
341
|
+
|| !isBoundedCounter(value.droppedRecords)
|
|
342
|
+
|| !validFatalTimestamp(value.timestamp)
|
|
343
|
+
|| !Array.isArray(value.stackFrames)
|
|
344
|
+
|| value.stackFrames.length > MAX_FATAL_STACK_FRAMES) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const frames = value.stackFrames.map(validatedFatalFrame);
|
|
348
|
+
if (frames.some((frame) => frame === undefined)) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
corruptRecords: value.corruptRecords,
|
|
353
|
+
droppedRecords: value.droppedRecords,
|
|
354
|
+
errorName: value.errorName,
|
|
355
|
+
id: value.id,
|
|
356
|
+
schemaVersion: FATAL_RECORD_SCHEMA_VERSION,
|
|
357
|
+
stackFrames: frames,
|
|
358
|
+
timestamp: value.timestamp
|
|
359
|
+
};
|
|
360
|
+
} catch {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function validatedFatalFrame(value) {
|
|
366
|
+
try {
|
|
367
|
+
if (!isPlainObject(value)
|
|
368
|
+
|| !hasExactKeys(value, ["column", "filename", "line"])
|
|
369
|
+
|| !validFatalFilename(value.filename)
|
|
370
|
+
|| positiveInteger(value.line) === undefined
|
|
371
|
+
|| positiveInteger(value.column) === undefined) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
column: value.column,
|
|
376
|
+
filename: value.filename,
|
|
377
|
+
line: value.line
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validFatalFilename(value) {
|
|
385
|
+
return typeof value === "string"
|
|
386
|
+
&& value.length > 0
|
|
387
|
+
&& utf8ByteLength(value) <= MAX_FATAL_FILENAME_BYTES
|
|
388
|
+
&& !hasControlCharacter(value)
|
|
389
|
+
&& !value.startsWith("/")
|
|
390
|
+
&& !/^[A-Za-z]:/u.test(value)
|
|
391
|
+
&& !value.includes("\\")
|
|
392
|
+
&& !value.includes("://")
|
|
393
|
+
&& !value.includes("?")
|
|
394
|
+
&& !value.includes("#")
|
|
395
|
+
&& !value.split("/").includes("..");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizedStoredFatalFilename(value) {
|
|
399
|
+
return typeof value === "string"
|
|
400
|
+
&& value.startsWith("/")
|
|
401
|
+
&& value.length > 1
|
|
402
|
+
&& value.indexOf("/", 1) === -1
|
|
403
|
+
? value.slice(1)
|
|
404
|
+
: value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function validFatalTimestamp(value) {
|
|
408
|
+
return typeof value === "string"
|
|
409
|
+
&& value.length >= 20
|
|
410
|
+
&& value.length <= 35
|
|
411
|
+
&& /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z$/u.test(value);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resultStatus(result) {
|
|
415
|
+
try {
|
|
416
|
+
return isPlainObject(result) && typeof result.status === "string"
|
|
417
|
+
? result.status
|
|
418
|
+
: undefined;
|
|
419
|
+
} catch {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function boundedResultCounter(result, property, fallback) {
|
|
425
|
+
const value = isPlainObject(result) ? safeReadProperty(result, property) : undefined;
|
|
426
|
+
return isBoundedCounter(value) ? value : fallback;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function hasExactKeys(value, expected) {
|
|
430
|
+
try {
|
|
431
|
+
const keys = Object.keys(value).sort();
|
|
432
|
+
return keys.length === expected.length
|
|
433
|
+
&& keys.every((key, index) => key === expected[index]);
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isBoundedCounter(value) {
|
|
440
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isPlainObject(value) {
|
|
444
|
+
if (value === null || typeof value !== "object") {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const prototype = Object.getPrototypeOf(value);
|
|
449
|
+
return prototype === Object.prototype || prototype === null;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function safeReadProperty(value, property) {
|
|
456
|
+
if (!isObjectLike(value)) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
return value[property];
|
|
461
|
+
} catch {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function safeFunction(value, property) {
|
|
467
|
+
const candidate = safeReadProperty(value, property);
|
|
468
|
+
return typeof candidate === "function" ? candidate : undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function hasControlCharacter(value) {
|
|
472
|
+
return Array.from(value).some((character) => {
|
|
473
|
+
const code = character.codePointAt(0);
|
|
474
|
+
return code !== undefined && (code <= 31 || code === 127);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function positiveInteger(value) {
|
|
479
|
+
if (!/^[1-9][0-9]*$/u.test(String(value))) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
const number = Number(value);
|
|
483
|
+
return Number.isSafeInteger(number) && number <= 2147483647 ? number : undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function nextFatalEventId() {
|
|
487
|
+
nextFatalSequence = incrementBounded(nextFatalSequence);
|
|
488
|
+
return `evt_rn_fatal_${Date.now().toString(36)}_${nextFatalSequence.toString(36)}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function incrementBounded(value) {
|
|
492
|
+
return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function emitDiagnostic(onDiagnostic, code) {
|
|
496
|
+
if (typeof onDiagnostic !== "function") {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
onDiagnostic(Object.freeze({ code }));
|
|
501
|
+
} catch {
|
|
502
|
+
// Diagnostics must never interfere with application error handling.
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function utf8ByteLength(value) {
|
|
507
|
+
let bytes = 0;
|
|
508
|
+
for (const character of value) {
|
|
509
|
+
const code = character.codePointAt(0);
|
|
510
|
+
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
511
|
+
}
|
|
512
|
+
return bytes;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function fatalHealthSnapshot(state) {
|
|
516
|
+
return Object.freeze({
|
|
517
|
+
acknowledgedRecords: state.acknowledgedRecords,
|
|
518
|
+
available: state.available,
|
|
519
|
+
corruptRecords: state.corruptRecords,
|
|
520
|
+
droppedRecords: state.droppedRecords,
|
|
521
|
+
lastOutcome: state.lastOutcome,
|
|
522
|
+
replayedRecords: state.replayedRecords,
|
|
523
|
+
storedRecords: state.storedRecords
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isObjectLike(value) {
|
|
528
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = { createFatalController };
|
package/global-errors.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { createReactNativeErrorEvent } = require("./index.cjs");
|
|
4
|
+
const { createFatalController } = require("./fatal-replay.cjs");
|
|
4
5
|
|
|
5
6
|
const AUTOMATIC_ERROR_MESSAGE = "React Native global JavaScript report";
|
|
6
7
|
const MAX_STACK_BYTES = 16 * 1024;
|
|
@@ -14,12 +15,18 @@ const SAFE_ERROR_NAMES = new Set([
|
|
|
14
15
|
"TypeError",
|
|
15
16
|
"URIError"
|
|
16
17
|
]);
|
|
18
|
+
const FATAL_SANITIZERS = Object.freeze({
|
|
19
|
+
errorName: safeErrorName,
|
|
20
|
+
isErrorName: (value) => SAFE_ERROR_NAMES.has(value),
|
|
21
|
+
stackFrame: safeStackFrameParts
|
|
22
|
+
});
|
|
17
23
|
const installations = new WeakMap();
|
|
18
24
|
let nextEventSequence = 0;
|
|
19
25
|
|
|
20
26
|
function installLogBrewReactNativeGlobalErrorHandler({
|
|
21
27
|
client,
|
|
22
28
|
errorUtils = globalThis?.ErrorUtils,
|
|
29
|
+
fatalStore,
|
|
23
30
|
onDiagnostic
|
|
24
31
|
} = {}) {
|
|
25
32
|
const issue = safeFunction(client, "issue");
|
|
@@ -54,6 +61,14 @@ function installLogBrewReactNativeGlobalErrorHandler({
|
|
|
54
61
|
lastOutcome: "idle",
|
|
55
62
|
suppressedEvents: 0
|
|
56
63
|
};
|
|
64
|
+
const fatal = createFatalController({
|
|
65
|
+
client,
|
|
66
|
+
eventMessage: AUTOMATIC_ERROR_MESSAGE,
|
|
67
|
+
fatalStore,
|
|
68
|
+
issue,
|
|
69
|
+
onDiagnostic,
|
|
70
|
+
sanitizers: FATAL_SANITIZERS
|
|
71
|
+
});
|
|
57
72
|
const capturedErrors = new WeakSet();
|
|
58
73
|
const handler = (error, isFatal) => {
|
|
59
74
|
if (!state.active) {
|
|
@@ -67,8 +82,12 @@ function installLogBrewReactNativeGlobalErrorHandler({
|
|
|
67
82
|
state.handling = true;
|
|
68
83
|
try {
|
|
69
84
|
if (isFatal === true) {
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
|
72
91
|
} else if (isObjectLike(error) && capturedErrors.has(error)) {
|
|
73
92
|
recordSuppression(state, onDiagnostic, "duplicate_capture_suppressed");
|
|
74
93
|
} else {
|
|
@@ -86,6 +105,12 @@ function installLogBrewReactNativeGlobalErrorHandler({
|
|
|
86
105
|
};
|
|
87
106
|
|
|
88
107
|
const installation = Object.freeze({
|
|
108
|
+
discardPendingFatalRecord() {
|
|
109
|
+
return fatal.discard();
|
|
110
|
+
},
|
|
111
|
+
fatalHealth() {
|
|
112
|
+
return fatal.health();
|
|
113
|
+
},
|
|
89
114
|
health() {
|
|
90
115
|
return healthSnapshot(state);
|
|
91
116
|
},
|
|
@@ -133,6 +158,7 @@ function installLogBrewReactNativeGlobalErrorHandler({
|
|
|
133
158
|
return inactiveInstallation();
|
|
134
159
|
}
|
|
135
160
|
installations.set(errorUtils, installation);
|
|
161
|
+
fatal.replay();
|
|
136
162
|
return installation;
|
|
137
163
|
}
|
|
138
164
|
|
|
@@ -207,6 +233,13 @@ function safeStack(error) {
|
|
|
207
233
|
}
|
|
208
234
|
|
|
209
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) {
|
|
210
243
|
let location = typeof line === "string" ? line.trim() : "";
|
|
211
244
|
if (!location) {
|
|
212
245
|
return undefined;
|
|
@@ -229,7 +262,11 @@ function safeStackFrame(line) {
|
|
|
229
262
|
if (!filename || lineNumber === undefined || column === undefined) {
|
|
230
263
|
return undefined;
|
|
231
264
|
}
|
|
232
|
-
return
|
|
265
|
+
return {
|
|
266
|
+
column,
|
|
267
|
+
filename,
|
|
268
|
+
line: lineNumber
|
|
269
|
+
};
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
function safeStackFilename(value) {
|
|
@@ -340,7 +377,18 @@ function inactiveInstallation() {
|
|
|
340
377
|
lastOutcome: "unavailable",
|
|
341
378
|
suppressedEvents: 0
|
|
342
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
|
+
});
|
|
343
389
|
return Object.freeze({
|
|
390
|
+
discardPendingFatalRecord: () => false,
|
|
391
|
+
fatalHealth: () => fatalSnapshot,
|
|
344
392
|
health: () => snapshot,
|
|
345
393
|
remove: () => false
|
|
346
394
|
});
|