@logbrew/react-native 0.1.10 → 0.1.12
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 +25 -3
- package/README.md +174 -4
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/apple-native-diagnostics.d.ts +52 -0
- package/apple-native-diagnostics.js +286 -0
- package/expo.cjs +46 -0
- package/expo.d.ts +16 -0
- package/expo.js +5 -0
- package/fatal-replay.cjs +12 -1
- package/global-errors.cjs +3 -1
- package/index.cjs +86 -2
- package/index.d.cts +7 -0
- package/index.d.ts +7 -0
- package/index.js +86 -2
- package/index.native.d.ts +47 -1
- package/index.native.js +81 -1
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
- package/ios/LBRNEventRecordStore.h +29 -0
- package/ios/LBRNEventRecordStore.m +746 -0
- package/ios/LBRNFatalStoreModule.mm +159 -14
- package/ios/LBRNPrivateStorage.h +7 -0
- package/ios/LBRNPrivateStorage.m +38 -0
- package/package.json +47 -4
- package/persistent-delivery.native.js +282 -0
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
- package/src/NativeLogBrewFatalStore.ts +9 -0
|
@@ -9,10 +9,15 @@ import com.facebook.react.bridge.ReadableType;
|
|
|
9
9
|
import com.facebook.react.bridge.WritableArray;
|
|
10
10
|
import com.facebook.react.bridge.WritableMap;
|
|
11
11
|
import java.io.File;
|
|
12
|
+
import java.nio.charset.StandardCharsets;
|
|
13
|
+
import java.security.MessageDigest;
|
|
14
|
+
import java.security.NoSuchAlgorithmException;
|
|
12
15
|
import java.util.ArrayList;
|
|
13
16
|
import java.util.Arrays;
|
|
17
|
+
import java.util.HashMap;
|
|
14
18
|
import java.util.HashSet;
|
|
15
19
|
import java.util.List;
|
|
20
|
+
import java.util.Map;
|
|
16
21
|
import java.util.Set;
|
|
17
22
|
|
|
18
23
|
final class FatalStoreModuleImpl {
|
|
@@ -32,9 +37,12 @@ final class FatalStoreModuleImpl {
|
|
|
32
37
|
new HashSet<>(Arrays.asList("filename", "line", "column"));
|
|
33
38
|
|
|
34
39
|
private final FatalRecordStore store;
|
|
40
|
+
private final File eventStoreParent;
|
|
41
|
+
private final Map<String, EventRecordStore> eventStores = new HashMap<>();
|
|
35
42
|
|
|
36
43
|
FatalStoreModuleImpl(ReactApplicationContext context) {
|
|
37
44
|
File root = context.getNoBackupFilesDir();
|
|
45
|
+
eventStoreParent = root;
|
|
38
46
|
store =
|
|
39
47
|
root == null
|
|
40
48
|
? null
|
|
@@ -43,6 +51,66 @@ final class FatalStoreModuleImpl {
|
|
|
43
51
|
new AndroidParentDirectorySync());
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
WritableMap loadEventRecords(String queueKey) {
|
|
55
|
+
try {
|
|
56
|
+
EventRecordStore eventStore = eventStore(queueKey);
|
|
57
|
+
return eventStore == null ? status("storage_error") : eventResultMap(eventStore.load());
|
|
58
|
+
} catch (RuntimeException error) {
|
|
59
|
+
return status("storage_error");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
WritableMap appendEventRecord(String queueKey, String serializedEvent, double eventBytes) {
|
|
64
|
+
try {
|
|
65
|
+
EventRecordStore eventStore = eventStore(queueKey);
|
|
66
|
+
Integer byteCount = integer(eventBytes);
|
|
67
|
+
return eventStore == null || byteCount == null
|
|
68
|
+
? status("storage_error")
|
|
69
|
+
: eventResultMap(eventStore.append(serializedEvent, byteCount));
|
|
70
|
+
} catch (RuntimeException error) {
|
|
71
|
+
return status("storage_error");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
WritableMap acknowledgeEventRecords(String queueKey, double count) {
|
|
76
|
+
try {
|
|
77
|
+
EventRecordStore eventStore = eventStore(queueKey);
|
|
78
|
+
Integer recordCount = integer(count);
|
|
79
|
+
return eventStore == null || recordCount == null
|
|
80
|
+
? status("storage_error")
|
|
81
|
+
: eventResultMap(eventStore.acknowledge(recordCount));
|
|
82
|
+
} catch (RuntimeException error) {
|
|
83
|
+
return status("storage_error");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
WritableMap purgeEventRecords(String queueKey) {
|
|
88
|
+
try {
|
|
89
|
+
EventRecordStore eventStore = eventStore(queueKey);
|
|
90
|
+
return eventStore == null ? status("storage_error") : eventResultMap(eventStore.purge());
|
|
91
|
+
} catch (RuntimeException error) {
|
|
92
|
+
return status("storage_error");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
synchronized WritableMap closeEventStore(String queueKey) {
|
|
97
|
+
String queueHash = queueHash(queueKey);
|
|
98
|
+
if (queueHash == null) {
|
|
99
|
+
return status("storage_error");
|
|
100
|
+
}
|
|
101
|
+
EventRecordStore eventStore = eventStores.get(queueHash);
|
|
102
|
+
if (eventStore == null) {
|
|
103
|
+
return status("closed");
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return eventResultMap(eventStore.close());
|
|
107
|
+
} catch (RuntimeException error) {
|
|
108
|
+
return status("storage_error");
|
|
109
|
+
} finally {
|
|
110
|
+
eventStores.remove(queueHash);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
46
114
|
WritableMap writeFatalRecord(ReadableMap input) {
|
|
47
115
|
if (store == null) {
|
|
48
116
|
return status("storage_error");
|
|
@@ -227,4 +295,65 @@ final class FatalStoreModuleImpl {
|
|
|
227
295
|
output.putString("status", value);
|
|
228
296
|
return output;
|
|
229
297
|
}
|
|
298
|
+
|
|
299
|
+
private synchronized EventRecordStore eventStore(String queueKey) {
|
|
300
|
+
String queueHash = queueHash(queueKey);
|
|
301
|
+
if (eventStoreParent == null || queueHash == null) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
EventRecordStore existing = eventStores.get(queueHash);
|
|
305
|
+
if (existing != null) {
|
|
306
|
+
return existing;
|
|
307
|
+
}
|
|
308
|
+
EventRecordStore created =
|
|
309
|
+
new EventRecordStore(
|
|
310
|
+
new File(eventStoreParent, "logbrew-events-v1-" + queueHash),
|
|
311
|
+
new AndroidParentDirectorySync());
|
|
312
|
+
eventStores.put(queueHash, created);
|
|
313
|
+
return created;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private static String queueHash(String queueKey) {
|
|
317
|
+
if (queueKey == null || queueKey.trim().isEmpty()) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
byte[] keyBytes = queueKey.getBytes(StandardCharsets.UTF_8);
|
|
321
|
+
if (keyBytes.length == 0 || keyBytes.length > 4096) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
byte[] digest = MessageDigest.getInstance("SHA-256").digest(keyBytes);
|
|
326
|
+
StringBuilder value = new StringBuilder(digest.length * 2);
|
|
327
|
+
for (byte item : digest) {
|
|
328
|
+
value.append(String.format(java.util.Locale.ROOT, "%02x", item & 0xff));
|
|
329
|
+
}
|
|
330
|
+
return value.toString();
|
|
331
|
+
} catch (NoSuchAlgorithmException impossible) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private static Integer integer(double value) {
|
|
337
|
+
return Double.isFinite(value)
|
|
338
|
+
&& value >= 0
|
|
339
|
+
&& value <= Integer.MAX_VALUE
|
|
340
|
+
&& value == Math.rint(value)
|
|
341
|
+
? (int) value
|
|
342
|
+
: null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private static WritableMap eventResultMap(EventRecordStore.Result result) {
|
|
346
|
+
WritableMap output = status(result.status);
|
|
347
|
+
if ("loaded".equals(result.status)) {
|
|
348
|
+
WritableArray records = Arguments.createArray();
|
|
349
|
+
for (EventRecordStore.Record record : result.records) {
|
|
350
|
+
WritableMap value = Arguments.createMap();
|
|
351
|
+
value.putString("serializedEvent", record.serializedEvent);
|
|
352
|
+
value.putInt("eventBytes", record.eventBytes);
|
|
353
|
+
records.pushMap(value);
|
|
354
|
+
}
|
|
355
|
+
output.putArray("records", records);
|
|
356
|
+
}
|
|
357
|
+
return output;
|
|
358
|
+
}
|
|
230
359
|
}
|
|
@@ -36,4 +36,30 @@ final class FatalStoreModule extends NativeLogBrewFatalStoreSpec {
|
|
|
36
36
|
public WritableMap discardFatalRecord() {
|
|
37
37
|
return implementation.discardFatalRecord();
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
@Override
|
|
41
|
+
public WritableMap loadEventRecords(String queueKey) {
|
|
42
|
+
return implementation.loadEventRecords(queueKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Override
|
|
46
|
+
public WritableMap appendEventRecord(
|
|
47
|
+
String queueKey, String serializedEvent, double eventBytes) {
|
|
48
|
+
return implementation.appendEventRecord(queueKey, serializedEvent, eventBytes);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Override
|
|
52
|
+
public WritableMap acknowledgeEventRecords(String queueKey, double count) {
|
|
53
|
+
return implementation.acknowledgeEventRecords(queueKey, count);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Override
|
|
57
|
+
public WritableMap purgeEventRecords(String queueKey) {
|
|
58
|
+
return implementation.purgeEventRecords(queueKey);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Override
|
|
62
|
+
public WritableMap closeEventStore(String queueKey) {
|
|
63
|
+
return implementation.closeEventStore(queueKey);
|
|
64
|
+
}
|
|
39
65
|
}
|
|
@@ -38,4 +38,30 @@ final class FatalStoreModule extends ReactContextBaseJavaModule {
|
|
|
38
38
|
public WritableMap discardFatalRecord() {
|
|
39
39
|
return implementation.discardFatalRecord();
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
43
|
+
public WritableMap loadEventRecords(String queueKey) {
|
|
44
|
+
return implementation.loadEventRecords(queueKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
48
|
+
public WritableMap appendEventRecord(
|
|
49
|
+
String queueKey, String serializedEvent, double eventBytes) {
|
|
50
|
+
return implementation.appendEventRecord(queueKey, serializedEvent, eventBytes);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
54
|
+
public WritableMap acknowledgeEventRecords(String queueKey, double count) {
|
|
55
|
+
return implementation.acknowledgeEventRecords(queueKey, count);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
59
|
+
public WritableMap purgeEventRecords(String queueKey) {
|
|
60
|
+
return implementation.purgeEventRecords(queueKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
64
|
+
public WritableMap closeEventStore(String queueKey) {
|
|
65
|
+
return implementation.closeEventStore(queueKey);
|
|
66
|
+
}
|
|
41
67
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type LogBrewAppleNativeDiagnosticsAuth =
|
|
2
|
+
| { apiKey: string; clientKey?: never }
|
|
3
|
+
| { clientKey: string; apiKey?: never };
|
|
4
|
+
|
|
5
|
+
export type LogBrewAppleNativeDiagnosticsConfiguration =
|
|
6
|
+
LogBrewAppleNativeDiagnosticsAuth & {
|
|
7
|
+
projectId: string;
|
|
8
|
+
release: string;
|
|
9
|
+
environment: string;
|
|
10
|
+
service: string;
|
|
11
|
+
/** Explicitly confirms LogBrew is the process's only native fatal-handler owner. */
|
|
12
|
+
fatalHandlerOwnership: "logbrew";
|
|
13
|
+
/** Enable app-hang capture with a threshold from 1 through 30 seconds. Omit to disable. */
|
|
14
|
+
hangThresholdSeconds?: number;
|
|
15
|
+
/** Event ingestion endpoint. Defaults to the hosted LogBrew endpoint. */
|
|
16
|
+
endpoint?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type LogBrewAppleNativeDiagnosticsStatus = Readonly<{
|
|
20
|
+
status: "already_installed" | "installed" | "not_installed" | "ready";
|
|
21
|
+
lifecycle: "failed" | "idle" | "installed" | "replaying" | "stopped" | "unknown";
|
|
22
|
+
pending: number;
|
|
23
|
+
acknowledged: number;
|
|
24
|
+
discarded: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export type LogBrewAppleNativeDiagnosticsReplayResult = Readonly<{
|
|
28
|
+
status: "replayed";
|
|
29
|
+
attempted: number;
|
|
30
|
+
acknowledged: number;
|
|
31
|
+
discarded: number;
|
|
32
|
+
pending: number;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export declare function installLogBrewAppleNativeDiagnostics(
|
|
36
|
+
configuration: LogBrewAppleNativeDiagnosticsConfiguration
|
|
37
|
+
): LogBrewAppleNativeDiagnosticsStatus;
|
|
38
|
+
|
|
39
|
+
export declare function replayLogBrewAppleNativeDiagnostics(): Promise<
|
|
40
|
+
LogBrewAppleNativeDiagnosticsReplayResult
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
export declare function getLogBrewAppleNativeDiagnosticsStatus():
|
|
44
|
+
LogBrewAppleNativeDiagnosticsStatus;
|
|
45
|
+
|
|
46
|
+
declare const defaultExport: {
|
|
47
|
+
getLogBrewAppleNativeDiagnosticsStatus: typeof getLogBrewAppleNativeDiagnosticsStatus;
|
|
48
|
+
installLogBrewAppleNativeDiagnostics: typeof installLogBrewAppleNativeDiagnostics;
|
|
49
|
+
replayLogBrewAppleNativeDiagnostics: typeof replayLogBrewAppleNativeDiagnostics;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default defaultExport;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { SdkError } from "@logbrew/sdk";
|
|
2
|
+
import { NativeModules, Platform, TurboModuleRegistry } from "react-native";
|
|
3
|
+
|
|
4
|
+
const INSTALL_STATUSES = new Set(["already_installed", "installed"]);
|
|
5
|
+
const LIFECYCLE_STATUSES = new Set([
|
|
6
|
+
"failed",
|
|
7
|
+
"idle",
|
|
8
|
+
"installed",
|
|
9
|
+
"replaying",
|
|
10
|
+
"stopped",
|
|
11
|
+
"unknown"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function installLogBrewAppleNativeDiagnostics(configuration = {}) {
|
|
15
|
+
requireApplePlatform();
|
|
16
|
+
const nativeModule = requireNativeModule();
|
|
17
|
+
const payload = normalizeConfiguration(configuration);
|
|
18
|
+
const result = callNative(nativeModule, "installNativeDiagnostics", payload);
|
|
19
|
+
return requireStatusResult("install", result, INSTALL_STATUSES);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function replayLogBrewAppleNativeDiagnostics() {
|
|
23
|
+
requireApplePlatform();
|
|
24
|
+
const nativeModule = requireNativeModule();
|
|
25
|
+
let result;
|
|
26
|
+
try {
|
|
27
|
+
result = await callNative(nativeModule, "replayNativeDiagnostics");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof SdkError) {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
throw new SdkError(
|
|
33
|
+
"native_diagnostics_failed",
|
|
34
|
+
"LogBrew Apple native diagnostics replay failed"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return requireReplayResult(result);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getLogBrewAppleNativeDiagnosticsStatus() {
|
|
41
|
+
requireApplePlatform();
|
|
42
|
+
const nativeModule = requireNativeModule();
|
|
43
|
+
const result = callNative(nativeModule, "nativeDiagnosticsStatus");
|
|
44
|
+
return requireStatusResult("status", result, new Set(["not_installed", "ready"]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeConfiguration(configuration) {
|
|
48
|
+
if (!configuration
|
|
49
|
+
|| Array.isArray(configuration)
|
|
50
|
+
|| typeof configuration !== "object") {
|
|
51
|
+
throw configurationError("configuration must be an object");
|
|
52
|
+
}
|
|
53
|
+
const allowedKeys = new Set([
|
|
54
|
+
"apiKey",
|
|
55
|
+
"clientKey",
|
|
56
|
+
"endpoint",
|
|
57
|
+
"environment",
|
|
58
|
+
"fatalHandlerOwnership",
|
|
59
|
+
"hangThresholdSeconds",
|
|
60
|
+
"projectId",
|
|
61
|
+
"release",
|
|
62
|
+
"service"
|
|
63
|
+
]);
|
|
64
|
+
for (const key of Object.keys(configuration)) {
|
|
65
|
+
if (!allowedKeys.has(key)) {
|
|
66
|
+
throw configurationError(`configuration contains unsupported key ${JSON.stringify(key)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(configuration, "apiKey")
|
|
70
|
+
&& Object.prototype.hasOwnProperty.call(configuration, "clientKey")) {
|
|
71
|
+
throw configurationError("apiKey and clientKey are mutually exclusive");
|
|
72
|
+
}
|
|
73
|
+
const authKey = exactString(
|
|
74
|
+
"apiKey or clientKey",
|
|
75
|
+
configuration.clientKey ?? configuration.apiKey,
|
|
76
|
+
4096
|
|
77
|
+
);
|
|
78
|
+
const projectId = exactString("projectId", configuration.projectId, 64);
|
|
79
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/u.test(projectId)) {
|
|
80
|
+
throw configurationError("projectId must be a lowercase UUID");
|
|
81
|
+
}
|
|
82
|
+
if (configuration.fatalHandlerOwnership !== "logbrew") {
|
|
83
|
+
throw configurationError(
|
|
84
|
+
"fatalHandlerOwnership must be the exact string logbrew after confirming no other native fatal handler is installed"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const payload = {
|
|
89
|
+
apiKey: authKey,
|
|
90
|
+
environment: exactString("environment", configuration.environment),
|
|
91
|
+
fatalHandlerOwnership: "logbrew",
|
|
92
|
+
projectId,
|
|
93
|
+
release: exactString("release", configuration.release),
|
|
94
|
+
service: exactString("service", configuration.service)
|
|
95
|
+
};
|
|
96
|
+
if (configuration.endpoint !== undefined) {
|
|
97
|
+
payload.endpoint = exactDeliveryEndpoint(configuration.endpoint);
|
|
98
|
+
}
|
|
99
|
+
if (configuration.hangThresholdSeconds !== undefined) {
|
|
100
|
+
const threshold = configuration.hangThresholdSeconds;
|
|
101
|
+
if (typeof threshold !== "number"
|
|
102
|
+
|| !Number.isFinite(threshold)
|
|
103
|
+
|| threshold < 1
|
|
104
|
+
|| threshold > 30) {
|
|
105
|
+
throw configurationError("hangThresholdSeconds must be a finite number from 1 through 30");
|
|
106
|
+
}
|
|
107
|
+
payload.hangThresholdSeconds = threshold;
|
|
108
|
+
}
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function requireApplePlatform() {
|
|
113
|
+
if (Platform?.OS !== "ios") {
|
|
114
|
+
throw new SdkError(
|
|
115
|
+
"unsupported_platform",
|
|
116
|
+
"LogBrew Apple native diagnostics are available only in an iOS native build"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function requireNativeModule() {
|
|
122
|
+
let nativeModule;
|
|
123
|
+
try {
|
|
124
|
+
nativeModule = TurboModuleRegistry?.get?.("LogBrewAppleDiagnostics")
|
|
125
|
+
?? NativeModules?.LogBrewAppleDiagnostics;
|
|
126
|
+
} catch {
|
|
127
|
+
nativeModule = undefined;
|
|
128
|
+
}
|
|
129
|
+
if (!nativeModule) {
|
|
130
|
+
throw new SdkError(
|
|
131
|
+
"native_diagnostics_unavailable",
|
|
132
|
+
"LogBrew Apple native diagnostics require the AppleDiagnostics CocoaPods subspec; Expo projects can add @logbrew/react-native/expo"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return nativeModule;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function callNative(nativeModule, method, ...args) {
|
|
139
|
+
if (typeof nativeModule?.[method] !== "function") {
|
|
140
|
+
throw new SdkError(
|
|
141
|
+
"native_diagnostics_unavailable",
|
|
142
|
+
`linked LogBrew Apple diagnostics do not implement ${method}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return nativeModule[method](...args);
|
|
147
|
+
} catch {
|
|
148
|
+
throw new SdkError(
|
|
149
|
+
"native_diagnostics_failed",
|
|
150
|
+
`LogBrew Apple native diagnostics ${method} failed`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function requireStatusResult(operation, result, allowedStatuses) {
|
|
156
|
+
if (isErrorResult(result)) {
|
|
157
|
+
throw new SdkError(
|
|
158
|
+
result.code,
|
|
159
|
+
`LogBrew Apple native diagnostics ${operation} failed with ${result.code}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!isPlainObject(result)
|
|
163
|
+
|| !allowedStatuses.has(result.status)
|
|
164
|
+
|| !LIFECYCLE_STATUSES.has(result.lifecycle)
|
|
165
|
+
|| !isCount(result.pending)
|
|
166
|
+
|| !isCount(result.acknowledged)
|
|
167
|
+
|| !isCount(result.discarded)) {
|
|
168
|
+
throw new SdkError(
|
|
169
|
+
"native_diagnostics_invalid_response",
|
|
170
|
+
`LogBrew Apple native diagnostics ${operation} returned an invalid response`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
acknowledged: result.acknowledged,
|
|
175
|
+
discarded: result.discarded,
|
|
176
|
+
lifecycle: result.lifecycle,
|
|
177
|
+
pending: result.pending,
|
|
178
|
+
status: result.status
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function requireReplayResult(result) {
|
|
183
|
+
if (isErrorResult(result)) {
|
|
184
|
+
throw new SdkError(
|
|
185
|
+
result.code,
|
|
186
|
+
`LogBrew Apple native diagnostics replay failed with ${result.code}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (!isPlainObject(result)
|
|
190
|
+
|| result.status !== "replayed"
|
|
191
|
+
|| !isCount(result.attempted)
|
|
192
|
+
|| !isCount(result.acknowledged)
|
|
193
|
+
|| !isCount(result.discarded)
|
|
194
|
+
|| !isCount(result.pending)
|
|
195
|
+
|| result.acknowledged + result.discarded > result.attempted) {
|
|
196
|
+
throw new SdkError(
|
|
197
|
+
"native_diagnostics_invalid_response",
|
|
198
|
+
"LogBrew Apple native diagnostics replay returned an invalid response"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
acknowledged: result.acknowledged,
|
|
203
|
+
attempted: result.attempted,
|
|
204
|
+
discarded: result.discarded,
|
|
205
|
+
pending: result.pending,
|
|
206
|
+
status: result.status
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isErrorResult(result) {
|
|
211
|
+
return isPlainObject(result)
|
|
212
|
+
&& result.status === "error"
|
|
213
|
+
&& typeof result.code === "string"
|
|
214
|
+
&& /^[a-z0-9_]{1,128}$/u.test(result.code);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isPlainObject(value) {
|
|
218
|
+
return value !== null && !Array.isArray(value) && typeof value === "object";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isCount(value) {
|
|
222
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function exactString(name, value, maxBytes = 256) {
|
|
226
|
+
if (typeof value !== "string"
|
|
227
|
+
|| value.length === 0
|
|
228
|
+
|| value.trim() !== value
|
|
229
|
+
|| hasControlCharacter(value)
|
|
230
|
+
|| utf8Length(value) > maxBytes) {
|
|
231
|
+
throw configurationError(`${name} must be a non-empty bounded string without surrounding whitespace or control characters`);
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function exactDeliveryEndpoint(value) {
|
|
237
|
+
const endpoint = exactString("endpoint", value, 2048);
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(endpoint);
|
|
241
|
+
} catch {
|
|
242
|
+
throw configurationError("endpoint must be an absolute HTTPS URL");
|
|
243
|
+
}
|
|
244
|
+
if (parsed.protocol !== "https:") {
|
|
245
|
+
throw configurationError("endpoint must use HTTPS");
|
|
246
|
+
}
|
|
247
|
+
if (parsed.username
|
|
248
|
+
|| parsed.password
|
|
249
|
+
|| parsed.search
|
|
250
|
+
|| parsed.hash
|
|
251
|
+
|| endpoint !== `${parsed.origin}${parsed.pathname}`) {
|
|
252
|
+
throw configurationError("endpoint must be a plain HTTPS path without embedded auth, query, or fragment values");
|
|
253
|
+
}
|
|
254
|
+
return endpoint;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hasControlCharacter(value) {
|
|
258
|
+
for (const scalar of value) {
|
|
259
|
+
const codePoint = scalar.codePointAt(0);
|
|
260
|
+
if (codePoint <= 0x1f || codePoint === 0x7f) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function utf8Length(value) {
|
|
268
|
+
let bytes = 0;
|
|
269
|
+
for (const scalar of value) {
|
|
270
|
+
const codePoint = scalar.codePointAt(0);
|
|
271
|
+
bytes += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
272
|
+
}
|
|
273
|
+
return bytes;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function configurationError(message) {
|
|
277
|
+
return new SdkError("configuration_error", `LogBrew Apple native diagnostics ${message}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const logBrewAppleNativeDiagnostics = {
|
|
281
|
+
getLogBrewAppleNativeDiagnosticsStatus,
|
|
282
|
+
installLogBrewAppleNativeDiagnostics,
|
|
283
|
+
replayLogBrewAppleNativeDiagnostics
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export default logBrewAppleNativeDiagnostics;
|
package/expo.cjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const PLUGIN_NAME = "@logbrew/react-native/expo";
|
|
2
|
+
const START_MARKER = " # @generated by @logbrew/react-native/expo";
|
|
3
|
+
const END_MARKER = " # @end @logbrew/react-native/expo";
|
|
4
|
+
const POD_BLOCK = [
|
|
5
|
+
START_MARKER,
|
|
6
|
+
" logbrew_react_native_path = File.dirname(`node --print \"require.resolve('@logbrew/react-native')\"`)",
|
|
7
|
+
" pod 'LogBrewReactNative/AppleNativeDiagnostics', :path => logbrew_react_native_path",
|
|
8
|
+
END_MARKER
|
|
9
|
+
].join("\n");
|
|
10
|
+
|
|
11
|
+
function withLogBrewReactNative(config, options = {}) {
|
|
12
|
+
const { withPodfile } = require("expo/config-plugins");
|
|
13
|
+
return withPodfile(config, (podfileConfig) => {
|
|
14
|
+
podfileConfig.modResults.contents = modifyPodfile(
|
|
15
|
+
podfileConfig.modResults.contents,
|
|
16
|
+
options?.appleNativeDiagnostics !== false
|
|
17
|
+
);
|
|
18
|
+
return podfileConfig;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function modifyPodfile(contents, enabled = true) {
|
|
23
|
+
if (typeof contents !== "string") {
|
|
24
|
+
throw new TypeError(`${PLUGIN_NAME} expected Podfile text`);
|
|
25
|
+
}
|
|
26
|
+
const withoutManagedBlock = contents.replace(
|
|
27
|
+
new RegExp(`${escapePattern(START_MARKER)}[\\s\\S]*?${escapePattern(END_MARKER)}\\n?`, "gu"),
|
|
28
|
+
""
|
|
29
|
+
);
|
|
30
|
+
if (!enabled) {
|
|
31
|
+
return withoutManagedBlock;
|
|
32
|
+
}
|
|
33
|
+
const targetPattern = /^(target\s+['"][^'"]+['"]\s+do\s*)$/mu;
|
|
34
|
+
if (!targetPattern.test(withoutManagedBlock)) {
|
|
35
|
+
throw new Error(`${PLUGIN_NAME} could not locate an iOS application target in the Podfile`);
|
|
36
|
+
}
|
|
37
|
+
return withoutManagedBlock.replace(targetPattern, `$1\n${POD_BLOCK}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function escapePattern(value) {
|
|
41
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = withLogBrewReactNative;
|
|
45
|
+
module.exports.modifyPodfile = modifyPodfile;
|
|
46
|
+
module.exports.withLogBrewReactNative = withLogBrewReactNative;
|
package/expo.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type LogBrewExpoPluginOptions = {
|
|
2
|
+
/** Add the opt-in Apple native crash and app-hang CocoaPods subspec. Defaults to true. */
|
|
3
|
+
appleNativeDiagnostics?: boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ExpoConfigLike = Record<string, unknown>;
|
|
7
|
+
export type LogBrewExpoConfigPlugin = (
|
|
8
|
+
config: ExpoConfigLike,
|
|
9
|
+
options?: LogBrewExpoPluginOptions
|
|
10
|
+
) => ExpoConfigLike;
|
|
11
|
+
|
|
12
|
+
export declare const withLogBrewReactNative: LogBrewExpoConfigPlugin;
|
|
13
|
+
export declare function modifyPodfile(contents: string, enabled?: boolean): string;
|
|
14
|
+
|
|
15
|
+
declare const defaultExport: LogBrewExpoConfigPlugin;
|
|
16
|
+
export default defaultExport;
|
package/expo.js
ADDED
package/fatal-replay.cjs
CHANGED
|
@@ -293,7 +293,11 @@ function fatalStackFrames(error, stackFrame) {
|
|
|
293
293
|
const frame = stackFrame(line);
|
|
294
294
|
const filename = frame ? normalizedStoredFatalFilename(frame.filename) : undefined;
|
|
295
295
|
if (filename && validFatalFilename(filename)) {
|
|
296
|
-
frames.push(Object.freeze({
|
|
296
|
+
frames.push(Object.freeze({
|
|
297
|
+
column: frame.column,
|
|
298
|
+
filename,
|
|
299
|
+
line: frame.line
|
|
300
|
+
}));
|
|
297
301
|
if (frames.length === MAX_FATAL_STACK_FRAMES) {
|
|
298
302
|
break;
|
|
299
303
|
}
|
|
@@ -304,6 +308,13 @@ function fatalStackFrames(error, stackFrame) {
|
|
|
304
308
|
|
|
305
309
|
function fatalEventAttributes(record, eventMessage) {
|
|
306
310
|
return Object.freeze({
|
|
311
|
+
exception: Object.freeze({
|
|
312
|
+
type: record.errorName,
|
|
313
|
+
mechanism: Object.freeze({
|
|
314
|
+
type: "react_native_error_utils",
|
|
315
|
+
handled: false
|
|
316
|
+
})
|
|
317
|
+
}),
|
|
307
318
|
level: "fatal",
|
|
308
319
|
message: eventMessage,
|
|
309
320
|
metadata: Object.freeze({
|
package/global-errors.cjs
CHANGED
|
@@ -196,7 +196,9 @@ function createAutomaticErrorEvent(error) {
|
|
|
196
196
|
}
|
|
197
197
|
const event = createReactNativeErrorEvent(sanitizedError, {
|
|
198
198
|
id: nextEventId(),
|
|
199
|
-
|
|
199
|
+
handled: false,
|
|
200
|
+
includeStack: false,
|
|
201
|
+
mechanism: "react_native_error_utils"
|
|
200
202
|
});
|
|
201
203
|
return {
|
|
202
204
|
...event,
|