@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.
Files changed (37) hide show
  1. package/LogBrewReactNative.podspec +1 -0
  2. package/README.md +70 -7
  3. package/android/CMakeLists.txt +7 -0
  4. package/android/build.gradle +12 -0
  5. package/android/src/main/cpp/android_diagnostics.cpp +363 -0
  6. package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
  7. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
  8. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
  9. package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
  10. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
  11. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +127 -156
  12. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
  13. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +17 -17
  14. package/android-native-diagnostics.d.ts +29 -0
  15. package/android-native-diagnostics.js +176 -0
  16. package/fatal-replay.cjs +35 -3
  17. package/global-errors.d.cts +3 -3
  18. package/global-errors.d.ts +3 -3
  19. package/global-errors.native.js +1 -12
  20. package/index.cjs +27 -5
  21. package/index.native.d.ts +4 -0
  22. package/index.native.js +29 -4
  23. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
  29. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
  30. package/ios/LBRNFatalStoreModule.mm +34 -86
  31. package/package.json +17 -5
  32. package/persistent-delivery.native.js +4 -3
  33. package/resource-fetch.js +5 -463
  34. package/src/NativeLogBrewFatalStore.ts +6 -4
  35. package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
  36. package/ios/LBRNFatalRecordStore.h +0 -25
  37. 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.util.ArrayList;
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> RECORD_KEYS =
25
+ private static final Set<String> ANDROID_DIAGNOSTICS_KEYS =
27
26
  new HashSet<>(
28
27
  Arrays.asList(
29
- "schemaVersion",
30
- "id",
31
- "timestamp",
32
- "errorName",
33
- "stackFrames",
34
- "droppedRecords",
35
- "corruptRecords"));
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
- store =
47
- root == null
48
- ? null
49
- : new FatalRecordStore(
50
- new File(root, "logbrew-fatal-js"),
51
- new AndroidParentDirectorySync());
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 writeFatalRecord(ReadableMap input) {
115
- if (store == null) {
116
- return status("storage_error");
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
- FatalRecordStore.Record record = readRecord(input);
120
- return record == null ? status("invalid_record") : resultMap(store.write(record));
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
- return status("storage_error");
144
+ androidDiagnostics = null;
145
+ return error("android_diagnostics_install_failed");
123
146
  }
124
147
  }
125
148
 
126
- WritableMap readFatalRecord() {
127
- if (store == null) {
128
- return status("storage_error");
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 discardFatalRecord() {
149
- if (store == null) {
150
- return status("storage_error");
156
+ synchronized WritableMap uninstallAndroidDiagnostics() {
157
+ if (androidDiagnostics == null) {
158
+ return androidDiagnosticsReceipt("not_installed", 0);
151
159
  }
152
160
  try {
153
- return resultMap(store.discard());
161
+ String statusValue = androidDiagnostics.uninstall();
162
+ WritableMap result = androidDiagnosticsResult(statusValue);
163
+ androidDiagnostics = null;
164
+ return result;
154
165
  } catch (RuntimeException error) {
155
- return status("storage_error");
166
+ return error("android_diagnostics_uninstall_failed");
156
167
  }
157
168
  }
158
169
 
159
- private static FatalRecordStore.Record readRecord(ReadableMap input) {
170
+ private static AndroidDiagnosticsInput readAndroidDiagnosticsInput(ReadableMap input) {
160
171
  if (input == null
161
- || !hasExactKeys(input, RECORD_KEYS)
162
- || !hasType(input, "schemaVersion", ReadableType.Number)
163
- || !hasType(input, "id", ReadableType.String)
164
- || !hasType(input, "timestamp", ReadableType.String)
165
- || !hasType(input, "errorName", ReadableType.String)
166
- || !hasType(input, "stackFrames", ReadableType.Array)
167
- || !hasType(input, "droppedRecords", ReadableType.Number)
168
- || !hasType(input, "corruptRecords", ReadableType.Number)) {
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 schemaVersion = integer(input, "schemaVersion");
172
- Integer droppedRecords = integer(input, "droppedRecords");
173
- Integer corruptRecords = integer(input, "corruptRecords");
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
- Integer line = integer(input, "line");
213
- Integer column = integer(input, "column");
214
- String filename = input.getString("filename");
215
- if (line == null || column == null || filename == null) {
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
- double value = map.getDouble(key);
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 resultMap(FatalRecordStore.Result result) {
250
- if (result == null || result.status == null) {
251
- return status("storage_error");
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 recordMap(FatalRecordStore.Record record) {
270
- WritableMap output = Arguments.createMap();
271
- output.putInt("schemaVersion", record.schemaVersion);
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 static WritableArray frameMaps(List<FatalRecordStore.Frame> frames) {
282
- WritableArray output = Arguments.createArray();
283
- for (FatalRecordStore.Frame frame : frames) {
284
- WritableMap value = Arguments.createMap();
285
- value.putString("filename", frame.filename);
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 status(String value) {
294
- WritableMap output = Arguments.createMap();
295
- output.putString("status", value);
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 WritableMap writeFatalRecord(ReadableMap record) {
22
- return implementation.writeFatalRecord(record);
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 WritableMap writeFatalRecord(ReadableMap record) {
24
- return implementation.writeFatalRecord(record);
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
+ }