@logbrew/react-native 0.1.22 → 0.1.24
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 +1 -0
- package/README.md +70 -7
- package/android/CMakeLists.txt +7 -0
- package/android/build.gradle +12 -0
- package/android/src/main/cpp/android_diagnostics.cpp +363 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +127 -156
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
- package/android-native-diagnostics.d.ts +29 -0
- package/android-native-diagnostics.js +176 -0
- package/fatal-replay.cjs +35 -3
- package/global-errors.d.cts +3 -3
- package/global-errors.d.ts +3 -3
- package/global-errors.native.js +1 -12
- package/index.cjs +27 -5
- package/index.native.d.ts +4 -0
- package/index.native.js +29 -4
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
- package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
- package/ios/LBRNFatalStoreModule.mm +34 -86
- package/package.json +17 -5
- package/persistent-delivery.native.js +4 -3
- package/resource-fetch.js +5 -463
- package/src/NativeLogBrewFatalStore.ts +6 -4
- package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
- package/ios/LBRNFatalRecordStore.h +0 -25
- package/ios/LBRNFatalRecordStore.m +0 -542
|
@@ -2,7 +2,6 @@ package co.logbrew.reactnative;
|
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.Arguments;
|
|
4
4
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
5
|
-
import com.facebook.react.bridge.ReadableArray;
|
|
6
5
|
import com.facebook.react.bridge.ReadableMap;
|
|
7
6
|
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
8
7
|
import com.facebook.react.bridge.ReadableType;
|
|
@@ -12,43 +11,51 @@ import java.io.File;
|
|
|
12
11
|
import java.nio.charset.StandardCharsets;
|
|
13
12
|
import java.security.MessageDigest;
|
|
14
13
|
import java.security.NoSuchAlgorithmException;
|
|
15
|
-
import java.
|
|
14
|
+
import java.security.SecureRandom;
|
|
16
15
|
import java.util.Arrays;
|
|
17
16
|
import java.util.HashMap;
|
|
18
17
|
import java.util.HashSet;
|
|
19
|
-
import java.util.List;
|
|
20
18
|
import java.util.Map;
|
|
21
19
|
import java.util.Set;
|
|
22
20
|
|
|
23
21
|
final class FatalStoreModuleImpl {
|
|
24
22
|
static final String NAME = "LogBrewFatalStore";
|
|
23
|
+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
|
25
24
|
|
|
26
|
-
private static final Set<String>
|
|
25
|
+
private static final Set<String> ANDROID_DIAGNOSTICS_KEYS =
|
|
27
26
|
new HashSet<>(
|
|
28
27
|
Arrays.asList(
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
private static final Set<String> FRAME_KEYS =
|
|
37
|
-
new HashSet<>(Arrays.asList("filename", "line", "column"));
|
|
28
|
+
"anrThresholdMs",
|
|
29
|
+
"clientKey",
|
|
30
|
+
"environment",
|
|
31
|
+
"fatalHandlerOwnership",
|
|
32
|
+
"projectId",
|
|
33
|
+
"release",
|
|
34
|
+
"service"));
|
|
38
35
|
|
|
39
|
-
private final FatalRecordStore store;
|
|
40
36
|
private final File eventStoreParent;
|
|
37
|
+
private final ReactApplicationContext context;
|
|
41
38
|
private final Map<String, EventRecordStore> eventStores = new HashMap<>();
|
|
39
|
+
private AndroidDiagnosticsRuntime androidDiagnostics;
|
|
42
40
|
|
|
43
41
|
FatalStoreModuleImpl(ReactApplicationContext context) {
|
|
42
|
+
this.context = context;
|
|
44
43
|
File root = context.getNoBackupFilesDir();
|
|
45
44
|
eventStoreParent = root;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
String secureRandomHex(double length) {
|
|
48
|
+
Integer byteCount = integer(length);
|
|
49
|
+
if (byteCount == null || byteCount < 1 || byteCount > 64) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
byte[] bytes = new byte[byteCount];
|
|
53
|
+
SECURE_RANDOM.nextBytes(bytes);
|
|
54
|
+
StringBuilder output = new StringBuilder(byteCount * 2);
|
|
55
|
+
for (byte value : bytes) {
|
|
56
|
+
output.append(String.format(java.util.Locale.ROOT, "%02x", value & 0xff));
|
|
57
|
+
}
|
|
58
|
+
return output.toString();
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
WritableMap loadEventRecords(String queueKey) {
|
|
@@ -106,127 +113,111 @@ final class FatalStoreModuleImpl {
|
|
|
106
113
|
return eventResultMap(eventStore.close());
|
|
107
114
|
} catch (RuntimeException error) {
|
|
108
115
|
return status("storage_error");
|
|
109
|
-
} finally {
|
|
110
|
-
eventStores.remove(queueHash);
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
WritableMap
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
synchronized WritableMap installAndroidDiagnostics(ReadableMap input) {
|
|
120
|
+
AndroidDiagnosticsInput configuration = readAndroidDiagnosticsInput(input);
|
|
121
|
+
EventRecordStore eventStore =
|
|
122
|
+
configuration == null ? null : eventStore(configuration.clientKey);
|
|
123
|
+
String queueHash =
|
|
124
|
+
configuration == null ? null : queueHash(configuration.clientKey);
|
|
125
|
+
if (eventStore == null || queueHash == null || eventStoreParent == null) {
|
|
126
|
+
return error("android_diagnostics_invalid_configuration");
|
|
127
|
+
}
|
|
128
|
+
if (androidDiagnostics != null && androidDiagnostics.installed()) {
|
|
129
|
+
return androidDiagnostics.matches(eventStore, configuration.configuration)
|
|
130
|
+
? androidDiagnosticsReceipt("already_installed", androidDiagnostics.pending())
|
|
131
|
+
: error("android_diagnostics_owned");
|
|
132
|
+
}
|
|
133
|
+
File storageRoot = new File(eventStoreParent, "logbrew-android-diagnostics-v1-" + queueHash);
|
|
134
|
+
if ((!storageRoot.isDirectory() && !storageRoot.mkdirs()) || !storageRoot.isDirectory()) {
|
|
135
|
+
return error("android_diagnostics_storage_failed");
|
|
117
136
|
}
|
|
118
137
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
androidDiagnostics =
|
|
139
|
+
new AndroidDiagnosticsRuntime(
|
|
140
|
+
context, storageRoot, eventStore, configuration.configuration);
|
|
141
|
+
String statusValue = androidDiagnostics.install();
|
|
142
|
+
return androidDiagnosticsResult(statusValue);
|
|
121
143
|
} catch (RuntimeException error) {
|
|
122
|
-
|
|
144
|
+
androidDiagnostics = null;
|
|
145
|
+
return error("android_diagnostics_install_failed");
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
WritableMap
|
|
127
|
-
if (
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
return resultMap(store.read());
|
|
132
|
-
} catch (RuntimeException error) {
|
|
133
|
-
return status("storage_error");
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
WritableMap acknowledgeFatalRecord(String recordId) {
|
|
138
|
-
if (store == null) {
|
|
139
|
-
return status("storage_error");
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
return resultMap(store.acknowledge(recordId));
|
|
143
|
-
} catch (RuntimeException error) {
|
|
144
|
-
return status("storage_error");
|
|
149
|
+
synchronized WritableMap androidDiagnosticsStatus() {
|
|
150
|
+
if (androidDiagnostics == null || !androidDiagnostics.installed()) {
|
|
151
|
+
return androidDiagnosticsReceipt("not_installed", 0);
|
|
145
152
|
}
|
|
153
|
+
return androidDiagnosticsResult("ready");
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
WritableMap
|
|
149
|
-
if (
|
|
150
|
-
return
|
|
156
|
+
synchronized WritableMap uninstallAndroidDiagnostics() {
|
|
157
|
+
if (androidDiagnostics == null) {
|
|
158
|
+
return androidDiagnosticsReceipt("not_installed", 0);
|
|
151
159
|
}
|
|
152
160
|
try {
|
|
153
|
-
|
|
161
|
+
String statusValue = androidDiagnostics.uninstall();
|
|
162
|
+
WritableMap result = androidDiagnosticsResult(statusValue);
|
|
163
|
+
androidDiagnostics = null;
|
|
164
|
+
return result;
|
|
154
165
|
} catch (RuntimeException error) {
|
|
155
|
-
return
|
|
166
|
+
return error("android_diagnostics_uninstall_failed");
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
|
|
159
|
-
private static
|
|
170
|
+
private static AndroidDiagnosticsInput readAndroidDiagnosticsInput(ReadableMap input) {
|
|
160
171
|
if (input == null
|
|
161
|
-
|| !hasExactKeys(input,
|
|
162
|
-
|| !hasType(input, "
|
|
163
|
-
|| !hasType(input, "
|
|
164
|
-
|| !hasType(input, "
|
|
165
|
-
|| !hasType(input, "
|
|
166
|
-
|| !hasType(input, "
|
|
167
|
-
|| !hasType(input, "
|
|
168
|
-
|| !hasType(input, "
|
|
172
|
+
|| !hasExactKeys(input, ANDROID_DIAGNOSTICS_KEYS)
|
|
173
|
+
|| !hasType(input, "anrThresholdMs", ReadableType.Number)
|
|
174
|
+
|| !hasType(input, "clientKey", ReadableType.String)
|
|
175
|
+
|| !hasType(input, "environment", ReadableType.String)
|
|
176
|
+
|| !hasType(input, "fatalHandlerOwnership", ReadableType.String)
|
|
177
|
+
|| !hasType(input, "projectId", ReadableType.String)
|
|
178
|
+
|| !hasType(input, "release", ReadableType.String)
|
|
179
|
+
|| !hasType(input, "service", ReadableType.String)
|
|
180
|
+
|| !"logbrew".equals(input.getString("fatalHandlerOwnership"))) {
|
|
169
181
|
return null;
|
|
170
182
|
}
|
|
171
|
-
Integer
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (schemaVersion == null || droppedRecords == null || corruptRecords == null) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
ReadableArray values = input.getArray("stackFrames");
|
|
179
|
-
if (values == null) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
List<FatalRecordStore.Frame> frames = new ArrayList<>(values.size());
|
|
183
|
-
for (int index = 0; index < values.size(); index += 1) {
|
|
184
|
-
if (values.getType(index) != ReadableType.Map) {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
ReadableMap value = values.getMap(index);
|
|
188
|
-
FatalRecordStore.Frame frame = readFrame(value);
|
|
189
|
-
if (frame == null) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
frames.add(frame);
|
|
193
|
-
}
|
|
194
|
-
return new FatalRecordStore.Record(
|
|
195
|
-
schemaVersion,
|
|
196
|
-
input.getString("id"),
|
|
197
|
-
input.getString("timestamp"),
|
|
198
|
-
input.getString("errorName"),
|
|
199
|
-
frames,
|
|
200
|
-
droppedRecords,
|
|
201
|
-
corruptRecords);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private static FatalRecordStore.Frame readFrame(ReadableMap input) {
|
|
205
|
-
if (input == null
|
|
206
|
-
|| !hasExactKeys(input, FRAME_KEYS)
|
|
207
|
-
|| !hasType(input, "filename", ReadableType.String)
|
|
208
|
-
|| !hasType(input, "line", ReadableType.Number)
|
|
209
|
-
|| !hasType(input, "column", ReadableType.Number)) {
|
|
183
|
+
Integer threshold = integer(input, "anrThresholdMs");
|
|
184
|
+
String clientKey = input.getString("clientKey");
|
|
185
|
+
if (threshold == null || clientKey == null || clientKey.trim().isEmpty()) {
|
|
210
186
|
return null;
|
|
211
187
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
188
|
+
try {
|
|
189
|
+
return new AndroidDiagnosticsInput(
|
|
190
|
+
clientKey,
|
|
191
|
+
new AndroidNativeDiagnostics.Configuration(
|
|
192
|
+
input.getString("projectId"),
|
|
193
|
+
input.getString("release"),
|
|
194
|
+
input.getString("environment"),
|
|
195
|
+
input.getString("service"),
|
|
196
|
+
android.os.Build.VERSION.RELEASE,
|
|
197
|
+
android.os.Build.MODEL,
|
|
198
|
+
androidArchitecture(),
|
|
199
|
+
threshold));
|
|
200
|
+
} catch (IllegalArgumentException error) {
|
|
216
201
|
return null;
|
|
217
202
|
}
|
|
218
|
-
if (filename.startsWith("/")
|
|
219
|
-
&& filename.indexOf('/', 1) < 0
|
|
220
|
-
&& filename.length() > 1) {
|
|
221
|
-
filename = filename.substring(1);
|
|
222
|
-
}
|
|
223
|
-
return new FatalRecordStore.Frame(filename, line, column);
|
|
224
203
|
}
|
|
225
204
|
|
|
226
205
|
private static boolean hasType(ReadableMap map, String key, ReadableType type) {
|
|
227
206
|
return map.hasKey(key) && !map.isNull(key) && map.getType(key) == type;
|
|
228
207
|
}
|
|
229
208
|
|
|
209
|
+
private static String androidArchitecture() {
|
|
210
|
+
String[] values = android.os.Process.is64Bit()
|
|
211
|
+
? android.os.Build.SUPPORTED_64_BIT_ABIS
|
|
212
|
+
: android.os.Build.SUPPORTED_32_BIT_ABIS;
|
|
213
|
+
String value = values.length == 0
|
|
214
|
+
? ""
|
|
215
|
+
: values[0];
|
|
216
|
+
return "arm64-v8a".equals(value)
|
|
217
|
+
? "arm64"
|
|
218
|
+
: "armeabi-v7a".equals(value) ? "arm" : value;
|
|
219
|
+
}
|
|
220
|
+
|
|
230
221
|
private static boolean hasExactKeys(ReadableMap map, Set<String> expected) {
|
|
231
222
|
Set<String> observed = new HashSet<>();
|
|
232
223
|
ReadableMapKeySetIterator iterator = map.keySetIterator();
|
|
@@ -237,62 +228,31 @@ final class FatalStoreModuleImpl {
|
|
|
237
228
|
}
|
|
238
229
|
|
|
239
230
|
private static Integer integer(ReadableMap map, String key) {
|
|
240
|
-
|
|
241
|
-
return Double.isFinite(value)
|
|
242
|
-
&& value >= 0
|
|
243
|
-
&& value <= Integer.MAX_VALUE
|
|
244
|
-
&& value == Math.rint(value)
|
|
245
|
-
? (int) value
|
|
246
|
-
: null;
|
|
231
|
+
return integer(map.getDouble(key));
|
|
247
232
|
}
|
|
248
233
|
|
|
249
|
-
private static WritableMap
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
WritableMap output = status(result.status);
|
|
254
|
-
if (result.recordId != null) {
|
|
255
|
-
output.putString("recordId", result.recordId);
|
|
256
|
-
}
|
|
257
|
-
if (result.droppedRecords > 0) {
|
|
258
|
-
output.putInt("droppedRecords", result.droppedRecords);
|
|
259
|
-
}
|
|
260
|
-
if (result.corruptRecords > 0) {
|
|
261
|
-
output.putInt("corruptRecords", result.corruptRecords);
|
|
262
|
-
}
|
|
263
|
-
if (result.record != null) {
|
|
264
|
-
output.putMap("record", recordMap(result.record));
|
|
265
|
-
}
|
|
234
|
+
private static WritableMap status(String value) {
|
|
235
|
+
WritableMap output = Arguments.createMap();
|
|
236
|
+
output.putString("status", value);
|
|
266
237
|
return output;
|
|
267
238
|
}
|
|
268
239
|
|
|
269
|
-
private static WritableMap
|
|
270
|
-
WritableMap output =
|
|
271
|
-
output.putInt("
|
|
272
|
-
output.putString("id", record.id);
|
|
273
|
-
output.putString("timestamp", record.timestamp);
|
|
274
|
-
output.putString("errorName", record.errorName);
|
|
275
|
-
output.putArray("stackFrames", frameMaps(record.stackFrames));
|
|
276
|
-
output.putInt("droppedRecords", record.droppedRecords);
|
|
277
|
-
output.putInt("corruptRecords", record.corruptRecords);
|
|
240
|
+
private static WritableMap androidDiagnosticsReceipt(String value, int pending) {
|
|
241
|
+
WritableMap output = status(value);
|
|
242
|
+
output.putInt("pending", pending);
|
|
278
243
|
return output;
|
|
279
244
|
}
|
|
280
245
|
|
|
281
|
-
private
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
value.putInt("line", frame.line);
|
|
287
|
-
value.putInt("column", frame.column);
|
|
288
|
-
output.pushMap(value);
|
|
289
|
-
}
|
|
290
|
-
return output;
|
|
246
|
+
private WritableMap androidDiagnosticsResult(String status) {
|
|
247
|
+
int pending = androidDiagnostics.pending();
|
|
248
|
+
return pending < 0
|
|
249
|
+
? error("android_diagnostics_storage_failed")
|
|
250
|
+
: androidDiagnosticsReceipt(status, pending);
|
|
291
251
|
}
|
|
292
252
|
|
|
293
|
-
private static WritableMap
|
|
294
|
-
WritableMap output =
|
|
295
|
-
output.putString("
|
|
253
|
+
private static WritableMap error(String code) {
|
|
254
|
+
WritableMap output = status("error");
|
|
255
|
+
output.putString("code", code);
|
|
296
256
|
return output;
|
|
297
257
|
}
|
|
298
258
|
|
|
@@ -356,4 +316,15 @@ final class FatalStoreModuleImpl {
|
|
|
356
316
|
}
|
|
357
317
|
return output;
|
|
358
318
|
}
|
|
319
|
+
|
|
320
|
+
private static final class AndroidDiagnosticsInput {
|
|
321
|
+
final String clientKey;
|
|
322
|
+
final AndroidNativeDiagnostics.Configuration configuration;
|
|
323
|
+
|
|
324
|
+
AndroidDiagnosticsInput(
|
|
325
|
+
String clientKey, AndroidNativeDiagnostics.Configuration configuration) {
|
|
326
|
+
this.clientKey = clientKey;
|
|
327
|
+
this.configuration = configuration;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
359
330
|
}
|
|
@@ -18,23 +18,8 @@ final class FatalStoreModule extends NativeLogBrewFatalStoreSpec {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
@Override
|
|
21
|
-
public
|
|
22
|
-
return implementation.
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Override
|
|
26
|
-
public WritableMap readFatalRecord() {
|
|
27
|
-
return implementation.readFatalRecord();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@Override
|
|
31
|
-
public WritableMap acknowledgeFatalRecord(String recordId) {
|
|
32
|
-
return implementation.acknowledgeFatalRecord(recordId);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@Override
|
|
36
|
-
public WritableMap discardFatalRecord() {
|
|
37
|
-
return implementation.discardFatalRecord();
|
|
21
|
+
public String secureRandomHex(double length) {
|
|
22
|
+
return implementation.secureRandomHex(length);
|
|
38
23
|
}
|
|
39
24
|
|
|
40
25
|
@Override
|
|
@@ -62,4 +47,19 @@ final class FatalStoreModule extends NativeLogBrewFatalStoreSpec {
|
|
|
62
47
|
public WritableMap closeEventStore(String queueKey) {
|
|
63
48
|
return implementation.closeEventStore(queueKey);
|
|
64
49
|
}
|
|
50
|
+
|
|
51
|
+
@Override
|
|
52
|
+
public WritableMap installAndroidDiagnostics(ReadableMap configuration) {
|
|
53
|
+
return implementation.installAndroidDiagnostics(configuration);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Override
|
|
57
|
+
public WritableMap androidDiagnosticsStatus() {
|
|
58
|
+
return implementation.androidDiagnosticsStatus();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Override
|
|
62
|
+
public WritableMap uninstallAndroidDiagnostics() {
|
|
63
|
+
return implementation.uninstallAndroidDiagnostics();
|
|
64
|
+
}
|
|
65
65
|
}
|
|
@@ -20,23 +20,8 @@ final class FatalStoreModule extends ReactContextBaseJavaModule {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
23
|
-
public
|
|
24
|
-
return implementation.
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
28
|
-
public WritableMap readFatalRecord() {
|
|
29
|
-
return implementation.readFatalRecord();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
33
|
-
public WritableMap acknowledgeFatalRecord(String recordId) {
|
|
34
|
-
return implementation.acknowledgeFatalRecord(recordId);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
38
|
-
public WritableMap discardFatalRecord() {
|
|
39
|
-
return implementation.discardFatalRecord();
|
|
23
|
+
public String secureRandomHex(double length) {
|
|
24
|
+
return implementation.secureRandomHex(length);
|
|
40
25
|
}
|
|
41
26
|
|
|
42
27
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
@@ -64,4 +49,19 @@ final class FatalStoreModule extends ReactContextBaseJavaModule {
|
|
|
64
49
|
public WritableMap closeEventStore(String queueKey) {
|
|
65
50
|
return implementation.closeEventStore(queueKey);
|
|
66
51
|
}
|
|
52
|
+
|
|
53
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
54
|
+
public WritableMap installAndroidDiagnostics(ReadableMap configuration) {
|
|
55
|
+
return implementation.installAndroidDiagnostics(configuration);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
59
|
+
public WritableMap androidDiagnosticsStatus() {
|
|
60
|
+
return implementation.androidDiagnosticsStatus();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
64
|
+
public WritableMap uninstallAndroidDiagnostics() {
|
|
65
|
+
return implementation.uninstallAndroidDiagnostics();
|
|
66
|
+
}
|
|
67
67
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type LogBrewAndroidNativeDiagnosticsConfiguration = Readonly<{
|
|
2
|
+
anrThresholdMs?: number;
|
|
3
|
+
clientKey: string;
|
|
4
|
+
environment: string;
|
|
5
|
+
fatalHandlerOwnership: "logbrew";
|
|
6
|
+
projectId: string;
|
|
7
|
+
release: string;
|
|
8
|
+
service: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export type LogBrewAndroidNativeDiagnosticsReceipt = Readonly<{
|
|
12
|
+
pending: number;
|
|
13
|
+
status:
|
|
14
|
+
| "already_installed"
|
|
15
|
+
| "installed"
|
|
16
|
+
| "not_installed"
|
|
17
|
+
| "ready"
|
|
18
|
+
| "uninstalled";
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export declare function installLogBrewAndroidNativeDiagnostics(
|
|
22
|
+
configuration: LogBrewAndroidNativeDiagnosticsConfiguration
|
|
23
|
+
): LogBrewAndroidNativeDiagnosticsReceipt;
|
|
24
|
+
|
|
25
|
+
export declare function getLogBrewAndroidNativeDiagnosticsStatus():
|
|
26
|
+
LogBrewAndroidNativeDiagnosticsReceipt;
|
|
27
|
+
|
|
28
|
+
export declare function uninstallLogBrewAndroidNativeDiagnostics():
|
|
29
|
+
LogBrewAndroidNativeDiagnosticsReceipt;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { SdkError } from "@logbrew/sdk";
|
|
2
|
+
import { NativeModules, Platform, TurboModuleRegistry } from "react-native";
|
|
3
|
+
|
|
4
|
+
const INSTALL_KEYS = new Set([
|
|
5
|
+
"anrThresholdMs",
|
|
6
|
+
"clientKey",
|
|
7
|
+
"environment",
|
|
8
|
+
"fatalHandlerOwnership",
|
|
9
|
+
"projectId",
|
|
10
|
+
"release",
|
|
11
|
+
"service"
|
|
12
|
+
]);
|
|
13
|
+
const RECEIPT_KEYS = new Set(["pending", "status"]);
|
|
14
|
+
|
|
15
|
+
export function installLogBrewAndroidNativeDiagnostics(configuration = {}) {
|
|
16
|
+
requireAndroid();
|
|
17
|
+
return receipt(
|
|
18
|
+
"install",
|
|
19
|
+
call("installAndroidDiagnostics", normalizeConfiguration(configuration)),
|
|
20
|
+
new Set(["already_installed", "installed"])
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getLogBrewAndroidNativeDiagnosticsStatus() {
|
|
25
|
+
requireAndroid();
|
|
26
|
+
return receipt(
|
|
27
|
+
"status",
|
|
28
|
+
call("androidDiagnosticsStatus"),
|
|
29
|
+
new Set(["not_installed", "ready"])
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function uninstallLogBrewAndroidNativeDiagnostics() {
|
|
34
|
+
requireAndroid();
|
|
35
|
+
return receipt(
|
|
36
|
+
"uninstall",
|
|
37
|
+
call("uninstallAndroidDiagnostics"),
|
|
38
|
+
new Set(["not_installed", "uninstalled"])
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeConfiguration(value) {
|
|
43
|
+
if (!isObject(value)) {
|
|
44
|
+
throw configurationError("configuration must be an object");
|
|
45
|
+
}
|
|
46
|
+
for (const key of Object.keys(value)) {
|
|
47
|
+
if (!INSTALL_KEYS.has(key)) {
|
|
48
|
+
throw configurationError("configuration contains an unsupported key");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (value.fatalHandlerOwnership !== "logbrew") {
|
|
52
|
+
throw configurationError(
|
|
53
|
+
"fatalHandlerOwnership must be logbrew after removing every other Android fatal handler"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const projectId = exactText(value.projectId, 64, "projectId");
|
|
57
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(projectId)) {
|
|
58
|
+
throw configurationError("projectId must be a lowercase UUID");
|
|
59
|
+
}
|
|
60
|
+
const threshold = value.anrThresholdMs ?? 5000;
|
|
61
|
+
if (!Number.isSafeInteger(threshold) || threshold < 2000 || threshold > 60000) {
|
|
62
|
+
throw configurationError("anrThresholdMs must be an integer from 2000 through 60000");
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
anrThresholdMs: threshold,
|
|
66
|
+
clientKey: exactText(value.clientKey, 4096, "clientKey"),
|
|
67
|
+
environment: exactText(value.environment, 128, "environment"),
|
|
68
|
+
fatalHandlerOwnership: "logbrew",
|
|
69
|
+
projectId,
|
|
70
|
+
release: exactText(value.release, 256, "release"),
|
|
71
|
+
service: exactScope(value.service, "service")
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function exactScope(value, name) {
|
|
76
|
+
const text = exactText(value, 128, name);
|
|
77
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(text)
|
|
78
|
+
|| text.includes("..")
|
|
79
|
+
|| text.includes("//")) {
|
|
80
|
+
throw configurationError(`${name} must be a bounded deployment identifier`);
|
|
81
|
+
}
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function exactText(value, maximumBytes, name) {
|
|
86
|
+
if (typeof value !== "string"
|
|
87
|
+
|| value.length === 0
|
|
88
|
+
|| value.trim() !== value
|
|
89
|
+
|| controlCharacter(value)
|
|
90
|
+
|| utf8Length(value) > maximumBytes) {
|
|
91
|
+
throw configurationError(`${name} must be a bounded non-empty string`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function requireAndroid() {
|
|
97
|
+
if (Platform?.OS !== "android") {
|
|
98
|
+
throw new SdkError(
|
|
99
|
+
"unsupported_platform",
|
|
100
|
+
"LogBrew Android native diagnostics require an Android native build"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function call(method, ...args) {
|
|
106
|
+
let nativeModule;
|
|
107
|
+
try {
|
|
108
|
+
nativeModule = TurboModuleRegistry?.get?.("LogBrewFatalStore")
|
|
109
|
+
?? NativeModules?.LogBrewFatalStore;
|
|
110
|
+
} catch {
|
|
111
|
+
nativeModule = undefined;
|
|
112
|
+
}
|
|
113
|
+
if (typeof nativeModule?.[method] !== "function") {
|
|
114
|
+
throw new SdkError(
|
|
115
|
+
"native_diagnostics_unavailable",
|
|
116
|
+
`linked LogBrew Android diagnostics do not implement ${method}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return nativeModule[method](...args);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new SdkError(
|
|
123
|
+
"native_diagnostics_failed",
|
|
124
|
+
`LogBrew Android native diagnostics ${method} failed`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function receipt(operation, value, statuses) {
|
|
130
|
+
const keys = isObject(value) ? Object.keys(value) : [];
|
|
131
|
+
if (isObject(value)
|
|
132
|
+
&& value.status === "error"
|
|
133
|
+
&& typeof value.code === "string"
|
|
134
|
+
&& /^[a-z0-9_]{1,128}$/u.test(value.code)) {
|
|
135
|
+
throw new SdkError(
|
|
136
|
+
value.code,
|
|
137
|
+
`LogBrew Android native diagnostics ${operation} failed with ${value.code}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!isObject(value)
|
|
141
|
+
|| keys.length !== RECEIPT_KEYS.size
|
|
142
|
+
|| !keys.every((key) => RECEIPT_KEYS.has(key))
|
|
143
|
+
|| !statuses.has(value.status)
|
|
144
|
+
|| !Number.isSafeInteger(value.pending)
|
|
145
|
+
|| value.pending < 0) {
|
|
146
|
+
throw new SdkError(
|
|
147
|
+
"native_diagnostics_invalid_response",
|
|
148
|
+
`LogBrew Android native diagnostics ${operation} returned an invalid response`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze({ pending: value.pending, status: value.status });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isObject(value) {
|
|
155
|
+
return value !== null && !Array.isArray(value) && typeof value === "object";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function controlCharacter(value) {
|
|
159
|
+
return Array.from(value).some((character) => {
|
|
160
|
+
const code = character.codePointAt(0);
|
|
161
|
+
return code <= 31 || (code >= 127 && code <= 159);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function utf8Length(value) {
|
|
166
|
+
let length = 0;
|
|
167
|
+
for (const character of value) {
|
|
168
|
+
const code = character.codePointAt(0);
|
|
169
|
+
length += code <= 127 ? 1 : code <= 2047 ? 2 : code <= 65535 ? 3 : 4;
|
|
170
|
+
}
|
|
171
|
+
return length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function configurationError(message) {
|
|
175
|
+
return new SdkError("configuration_error", `LogBrew Android native diagnostics ${message}`);
|
|
176
|
+
}
|