@logbrew/react-native 0.1.23 → 0.1.25
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/README.md +62 -5
- 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 +368 -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 +111 -156
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
- 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 +1 -1
- package/index.native.d.ts +4 -0
- package/index.native.js +21 -3
- 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 +15 -86
- package/package.json +18 -6
- package/persistent-delivery.native.js +4 -3
- package/src/NativeLogBrewFatalStore.ts +5 -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
|
@@ -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
|
+
}
|
package/fatal-replay.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const MAX_ADMITTED_FATAL_IDS = 256;
|
|
|
5
5
|
const MAX_FATAL_FILENAME_BYTES = 512;
|
|
6
6
|
const MAX_FATAL_STACK_BYTES = 16 * 1024;
|
|
7
7
|
const MAX_FATAL_STACK_FRAMES = 24;
|
|
8
|
+
const DURABLE_QUEUE = Symbol.for("co.logbrew.react-native.durable-event-queue");
|
|
8
9
|
|
|
9
10
|
const admittedFatalIdsByClient = new WeakMap();
|
|
10
11
|
let nextFatalSequence = 0;
|
|
@@ -18,12 +19,13 @@ function createFatalController({
|
|
|
18
19
|
sanitizers
|
|
19
20
|
}) {
|
|
20
21
|
const methods = fatalStoreMethods(fatalStore);
|
|
22
|
+
const directAdmission = !methods && safeReadProperty(client, DURABLE_QUEUE) === true;
|
|
21
23
|
const state = {
|
|
22
24
|
acknowledgedRecords: 0,
|
|
23
|
-
available: methods !== undefined,
|
|
25
|
+
available: methods !== undefined || directAdmission,
|
|
24
26
|
corruptRecords: 0,
|
|
25
27
|
droppedRecords: 0,
|
|
26
|
-
lastOutcome: methods ? "idle" : "unavailable",
|
|
28
|
+
lastOutcome: methods || directAdmission ? "idle" : "unavailable",
|
|
27
29
|
replayedRecords: 0,
|
|
28
30
|
storedRecords: 0
|
|
29
31
|
};
|
|
@@ -37,6 +39,9 @@ function createFatalController({
|
|
|
37
39
|
return fatalHealthSnapshot(state);
|
|
38
40
|
},
|
|
39
41
|
replay() {
|
|
42
|
+
if (!methods) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
40
45
|
replayPendingFatal({
|
|
41
46
|
client,
|
|
42
47
|
eventMessage,
|
|
@@ -49,6 +54,11 @@ function createFatalController({
|
|
|
49
54
|
});
|
|
50
55
|
},
|
|
51
56
|
store(error) {
|
|
57
|
+
if (!methods) {
|
|
58
|
+
return directAdmission
|
|
59
|
+
? admitFatalError({ client, error, eventMessage, issue, onDiagnostic, sanitizers, state })
|
|
60
|
+
: false;
|
|
61
|
+
}
|
|
52
62
|
return storeFatalError({
|
|
53
63
|
error,
|
|
54
64
|
fatalStore,
|
|
@@ -61,6 +71,26 @@ function createFatalController({
|
|
|
61
71
|
};
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
function admitFatalError({ client, error, eventMessage, issue, onDiagnostic, sanitizers, state }) {
|
|
75
|
+
const record = createFatalRecord(error, sanitizers);
|
|
76
|
+
const before = admissionSnapshot(client);
|
|
77
|
+
try {
|
|
78
|
+
issue.call(client, record.id, record.timestamp, fatalEventAttributes(record, eventMessage));
|
|
79
|
+
} catch {
|
|
80
|
+
state.lastOutcome = "storage_error";
|
|
81
|
+
emitDiagnostic(onDiagnostic, "fatal_store_failed");
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!wasAdmitted(before, admissionSnapshot(client))) {
|
|
85
|
+
state.lastOutcome = "storage_error";
|
|
86
|
+
emitDiagnostic(onDiagnostic, "fatal_store_failed");
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
state.storedRecords = incrementBounded(state.storedRecords);
|
|
90
|
+
state.lastOutcome = "stored";
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
function fatalStoreMethods(fatalStore) {
|
|
65
95
|
if (!isObjectLike(fatalStore)) {
|
|
66
96
|
return undefined;
|
|
@@ -327,7 +357,9 @@ function fatalEventAttributes(record, eventMessage) {
|
|
|
327
357
|
replayed: true,
|
|
328
358
|
source: "react-native.global_error"
|
|
329
359
|
}),
|
|
330
|
-
|
|
360
|
+
...(record.stackFrames.length === 0
|
|
361
|
+
? {}
|
|
362
|
+
: { stackFrames: record.stackFrames.map((frame) => Object.freeze({ ...frame })) }),
|
|
331
363
|
title: eventMessage
|
|
332
364
|
});
|
|
333
365
|
}
|
package/global-errors.d.cts
CHANGED
|
@@ -240,9 +240,9 @@ export declare function installLogBrewReactNativePromiseRejectionTracker(
|
|
|
240
240
|
/**
|
|
241
241
|
* Install reversible automatic capture for React Native global JavaScript errors.
|
|
242
242
|
*
|
|
243
|
-
* The React Native conditional export
|
|
244
|
-
* Direct Node ESM/CJS callers can inject a compatible
|
|
245
|
-
*
|
|
243
|
+
* The React Native conditional export admits fatal reports into its persistent event queue
|
|
244
|
+
* before the previous handler runs. Direct Node ESM/CJS callers can inject a compatible
|
|
245
|
+
* fatal store explicitly.
|
|
246
246
|
* This helper does not claim local exactly-once delivery or install Promise rejection handling.
|
|
247
247
|
*/
|
|
248
248
|
export declare function installLogBrewReactNativeGlobalErrorHandler(
|
package/global-errors.d.ts
CHANGED
|
@@ -240,9 +240,9 @@ export declare function installLogBrewReactNativePromiseRejectionTracker(
|
|
|
240
240
|
/**
|
|
241
241
|
* Install reversible automatic capture for React Native global JavaScript errors.
|
|
242
242
|
*
|
|
243
|
-
* The React Native conditional export
|
|
244
|
-
* Direct Node ESM/CJS callers can inject a compatible
|
|
245
|
-
*
|
|
243
|
+
* The React Native conditional export admits fatal reports into its persistent event queue
|
|
244
|
+
* before the previous handler runs. Direct Node ESM/CJS callers can inject a compatible
|
|
245
|
+
* fatal store explicitly.
|
|
246
246
|
* This helper does not claim local exactly-once delivery or install Promise rejection handling.
|
|
247
247
|
*/
|
|
248
248
|
export declare function installLogBrewReactNativeGlobalErrorHandler(
|
package/global-errors.native.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { NativeModules, TurboModuleRegistry } from "react-native";
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
createLogBrewReactNativePromiseRejectionHandlers,
|
|
5
3
|
installLogBrewReactNativeGlobalErrorHandler as installPlatformNeutralHandler,
|
|
@@ -12,15 +10,6 @@ export {
|
|
|
12
10
|
|
|
13
11
|
const hermesTrackerAdapters = new WeakMap();
|
|
14
12
|
|
|
15
|
-
function defaultFatalStore() {
|
|
16
|
-
try {
|
|
17
|
-
return TurboModuleRegistry?.get?.("LogBrewFatalStore")
|
|
18
|
-
?? NativeModules?.LogBrewFatalStore;
|
|
19
|
-
} catch {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
function defaultPromiseRejectionTracker() {
|
|
25
14
|
let hermes;
|
|
26
15
|
let enable;
|
|
@@ -70,7 +59,7 @@ export function installLogBrewReactNativeGlobalErrorHandler(options = {}) {
|
|
|
70
59
|
}
|
|
71
60
|
return installPlatformNeutralHandler({
|
|
72
61
|
...forwarded,
|
|
73
|
-
fatalStore: hasInjectedStore ? forwarded.fatalStore :
|
|
62
|
+
fatalStore: hasInjectedStore ? forwarded.fatalStore : undefined
|
|
74
63
|
});
|
|
75
64
|
}
|
|
76
65
|
|
package/index.cjs
CHANGED
|
@@ -16,7 +16,7 @@ const {
|
|
|
16
16
|
} = require("./metadata.cjs");
|
|
17
17
|
|
|
18
18
|
const DEFAULT_SDK_NAME = "logbrew-react-native";
|
|
19
|
-
const DEFAULT_SDK_VERSION = "0.1.
|
|
19
|
+
const DEFAULT_SDK_VERSION = "0.1.25";
|
|
20
20
|
const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
|
|
21
21
|
const NATIVE_RANDOM_HEX = Symbol.for("co.logbrew.react-native.secure-random-hex");
|
|
22
22
|
const MAX_ACTION_NAME_LENGTH = 64;
|
package/index.native.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CreateLogBrewReactNativeClientConfig } from "./index.js";
|
|
|
3
3
|
|
|
4
4
|
export * from "./index.js";
|
|
5
5
|
export * from "./apple-native-diagnostics.js";
|
|
6
|
+
export * from "./android-native-diagnostics.js";
|
|
6
7
|
export {
|
|
7
8
|
installLogBrewReactNativeGlobalErrorHandler,
|
|
8
9
|
installLogBrewReactNativePromiseRejectionTracker
|
|
@@ -43,10 +44,13 @@ declare const defaultExport: Omit<
|
|
|
43
44
|
createLogBrewReactNativeClient: typeof createLogBrewReactNativeClient;
|
|
44
45
|
createDefaultLogBrewReactNativeClient: typeof createDefaultLogBrewReactNativeClient;
|
|
45
46
|
getLogBrewAppleNativeDiagnosticsStatus: typeof import("./apple-native-diagnostics.js").getLogBrewAppleNativeDiagnosticsStatus;
|
|
47
|
+
getLogBrewAndroidNativeDiagnosticsStatus: typeof import("./android-native-diagnostics.js").getLogBrewAndroidNativeDiagnosticsStatus;
|
|
46
48
|
installLogBrewAppleNativeDiagnostics: typeof import("./apple-native-diagnostics.js").installLogBrewAppleNativeDiagnostics;
|
|
49
|
+
installLogBrewAndroidNativeDiagnostics: typeof import("./android-native-diagnostics.js").installLogBrewAndroidNativeDiagnostics;
|
|
47
50
|
purgeLogBrewReactNativePersistentQueue: typeof purgeLogBrewReactNativePersistentQueue;
|
|
48
51
|
replayLogBrewAppleNativeDiagnostics: typeof import("./apple-native-diagnostics.js").replayLogBrewAppleNativeDiagnostics;
|
|
49
52
|
setLogBrewAppleNativeCrashContext: typeof import("./apple-native-diagnostics.js").setLogBrewAppleNativeCrashContext;
|
|
53
|
+
uninstallLogBrewAndroidNativeDiagnostics: typeof import("./android-native-diagnostics.js").uninstallLogBrewAndroidNativeDiagnostics;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
export default defaultExport;
|
package/index.native.js
CHANGED
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
setLogBrewAppleNativeCrashContext,
|
|
18
18
|
syncLogBrewAppleNativeCrashBreadcrumbs
|
|
19
19
|
} from "./apple-native-diagnostics.js";
|
|
20
|
+
import {
|
|
21
|
+
getLogBrewAndroidNativeDiagnosticsStatus,
|
|
22
|
+
installLogBrewAndroidNativeDiagnostics,
|
|
23
|
+
uninstallLogBrewAndroidNativeDiagnostics
|
|
24
|
+
} from "./android-native-diagnostics.js";
|
|
20
25
|
import {
|
|
21
26
|
purgeReactNativePersistentQueue,
|
|
22
27
|
resolveReactNativePersistentEventStore
|
|
@@ -31,6 +36,11 @@ export {
|
|
|
31
36
|
replayLogBrewAppleNativeDiagnostics,
|
|
32
37
|
setLogBrewAppleNativeCrashContext
|
|
33
38
|
};
|
|
39
|
+
export {
|
|
40
|
+
getLogBrewAndroidNativeDiagnosticsStatus,
|
|
41
|
+
installLogBrewAndroidNativeDiagnostics,
|
|
42
|
+
uninstallLogBrewAndroidNativeDiagnostics
|
|
43
|
+
};
|
|
34
44
|
|
|
35
45
|
export * from "./index.js";
|
|
36
46
|
|
|
@@ -40,6 +50,7 @@ if (typeof nativeRuntime?.secureRandomHex === "function") {
|
|
|
40
50
|
globalThis[Symbol.for("co.logbrew.react-native.secure-random-hex")] =
|
|
41
51
|
nativeRuntime.secureRandomHex.bind(nativeRuntime);
|
|
42
52
|
}
|
|
53
|
+
const DURABLE_QUEUE = Symbol.for("co.logbrew.react-native.durable-event-queue");
|
|
43
54
|
|
|
44
55
|
export function createLogBrewReactNativeClient(config = {}) {
|
|
45
56
|
const input = config !== null && typeof config === "object" ? config : {};
|
|
@@ -69,14 +80,18 @@ export function createLogBrewReactNativeClient(config = {}) {
|
|
|
69
80
|
hasExplicitPersistentQueue
|
|
70
81
|
});
|
|
71
82
|
try {
|
|
72
|
-
|
|
83
|
+
const client = createPlatformNeutralClient({
|
|
73
84
|
...forwarded,
|
|
74
85
|
apiKey,
|
|
75
86
|
clientKey,
|
|
76
87
|
eventStore: resolved.eventStore,
|
|
77
88
|
maxQueueBytes,
|
|
78
89
|
maxQueueSize
|
|
79
|
-
})
|
|
90
|
+
});
|
|
91
|
+
if (resolved.durable) {
|
|
92
|
+
Object.defineProperty(client, DURABLE_QUEUE, { value: true });
|
|
93
|
+
}
|
|
94
|
+
return bindAppleNativeCrashBreadcrumbs(client);
|
|
80
95
|
} catch (error) {
|
|
81
96
|
resolved.abort();
|
|
82
97
|
throw error;
|
|
@@ -205,11 +220,14 @@ const defaultExport = {
|
|
|
205
220
|
createDefaultLogBrewReactNativeClient,
|
|
206
221
|
createLogBrewReactNativeClient,
|
|
207
222
|
getDefaultReactNativeContext,
|
|
223
|
+
getLogBrewAndroidNativeDiagnosticsStatus,
|
|
208
224
|
getLogBrewAppleNativeDiagnosticsStatus,
|
|
209
225
|
installLogBrewAppleNativeDiagnostics,
|
|
226
|
+
installLogBrewAndroidNativeDiagnostics,
|
|
210
227
|
purgeLogBrewReactNativePersistentQueue,
|
|
211
228
|
replayLogBrewAppleNativeDiagnostics,
|
|
212
|
-
setLogBrewAppleNativeCrashContext
|
|
229
|
+
setLogBrewAppleNativeCrashContext,
|
|
230
|
+
uninstallLogBrewAndroidNativeDiagnostics
|
|
213
231
|
};
|
|
214
232
|
|
|
215
233
|
export default defaultExport;
|
|
@@ -3,7 +3,7 @@ import Foundation
|
|
|
3
3
|
@objc(LBRNAppleNativeDiagnostics)
|
|
4
4
|
public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
|
|
5
5
|
private static let shared = LBRNAppleNativeDiagnostics()
|
|
6
|
-
private static let sdkVersion = "0.1.
|
|
6
|
+
private static let sdkVersion = "0.1.25"
|
|
7
7
|
|
|
8
8
|
private let lock = NSLock()
|
|
9
9
|
private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
|
|
@@ -14,9 +14,7 @@ public enum LogBrewLoggerLevel: String, Sendable {
|
|
|
14
14
|
|
|
15
15
|
var logBrewLevel: LogLevel {
|
|
16
16
|
switch self {
|
|
17
|
-
case .trace, .debug:
|
|
18
|
-
.info
|
|
19
|
-
case .info, .notice:
|
|
17
|
+
case .trace, .debug, .info, .notice:
|
|
20
18
|
.info
|
|
21
19
|
case .warning:
|
|
22
20
|
.warning
|
|
@@ -73,7 +71,7 @@ public final class LogBrewLogger {
|
|
|
73
71
|
baseMetadata = metadata ?? [:]
|
|
74
72
|
self.transport = transport
|
|
75
73
|
self.flushOnLog = flushOnLog
|
|
76
|
-
self.timestampProvider = timestampProvider ??
|
|
74
|
+
self.timestampProvider = timestampProvider ?? currentTelemetryTimestamp
|
|
77
75
|
self.onError = onError
|
|
78
76
|
}
|
|
79
77
|
|
|
@@ -246,12 +244,6 @@ public final class LogBrewLogger {
|
|
|
246
244
|
}
|
|
247
245
|
return merged
|
|
248
246
|
}
|
|
249
|
-
|
|
250
|
-
private static func defaultTimestamp() -> String {
|
|
251
|
-
let formatter = ISO8601DateFormatter()
|
|
252
|
-
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
253
|
-
return formatter.string(from: Date())
|
|
254
|
-
}
|
|
255
247
|
}
|
|
256
248
|
|
|
257
249
|
private struct LogBrewLoggerCall {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
public extension LogBrewClient {
|
|
6
|
+
/// Runs one app-owned operation under a root or child trace and records its span.
|
|
7
|
+
@discardableResult
|
|
8
|
+
func withOperation<Result>(
|
|
9
|
+
_ name: String,
|
|
10
|
+
metadata: Metadata? = nil,
|
|
11
|
+
onOperationError: ((any Error) -> Void)? = nil,
|
|
12
|
+
onCaptureError: ((any Error) -> Void)? = nil,
|
|
13
|
+
operation: () async throws -> Result,
|
|
14
|
+
) async throws -> Result {
|
|
15
|
+
let operationName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
16
|
+
try requireNonEmpty("operation name", operationName)
|
|
17
|
+
let context = LogBrewTrace.current.map(LogBrewTrace.childContext(of:)) ?? LogBrewTrace
|
|
18
|
+
.continueOrCreateContext(fromTraceparent: nil)
|
|
19
|
+
let startedAtMs = ProcessInfo.processInfo.systemUptime * 1000
|
|
20
|
+
func finish(_ error: (any Error)? = nil) {
|
|
21
|
+
do {
|
|
22
|
+
var evidence = metadata ?? [:]
|
|
23
|
+
evidence["source"] = .string("swift.operation")
|
|
24
|
+
if let error {
|
|
25
|
+
evidence["errorType"] = .string(String(reflecting: type(of: error)))
|
|
26
|
+
}
|
|
27
|
+
try span(
|
|
28
|
+
"swift_operation_span_\(context.spanId)",
|
|
29
|
+
timestamp: currentTelemetryTimestamp(),
|
|
30
|
+
attributes: LogBrewTrace.spanAttributes(
|
|
31
|
+
name: operationName,
|
|
32
|
+
status: error == nil ? .ok : .error,
|
|
33
|
+
durationMs: max(0, ProcessInfo.processInfo.systemUptime * 1000 - startedAtMs),
|
|
34
|
+
metadata: evidence,
|
|
35
|
+
context: context,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
} catch {
|
|
39
|
+
onCaptureError?(error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return try await LogBrewTrace.withContext(context) {
|
|
44
|
+
do {
|
|
45
|
+
let result = try await operation()
|
|
46
|
+
finish()
|
|
47
|
+
return result
|
|
48
|
+
} catch {
|
|
49
|
+
let operationError = error
|
|
50
|
+
onOperationError?(operationError)
|
|
51
|
+
finish(operationError)
|
|
52
|
+
throw operationError
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -23,7 +23,6 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
|
|
|
23
23
|
nowMsProvider: (@Sendable () -> Double)? = nil,
|
|
24
24
|
onCaptureError: (@Sendable (any Error) -> Void)? = nil,
|
|
25
25
|
) throws {
|
|
26
|
-
let loader = LogBrewURLSessionDataLoader(session: session)
|
|
27
26
|
try self.init(
|
|
28
27
|
client: client,
|
|
29
28
|
eventIDPrefix: eventIDPrefix,
|
|
@@ -31,7 +30,7 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
|
|
|
31
30
|
nowMsProvider: nowMsProvider,
|
|
32
31
|
onCaptureError: onCaptureError,
|
|
33
32
|
dataLoader: { request in
|
|
34
|
-
try await
|
|
33
|
+
try await session.data(for: request)
|
|
35
34
|
},
|
|
36
35
|
)
|
|
37
36
|
}
|
|
@@ -48,7 +47,7 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
|
|
|
48
47
|
try requireNonEmpty("URLSession tracer eventIDPrefix", normalizedPrefix)
|
|
49
48
|
self.client = client
|
|
50
49
|
self.eventIDPrefix = normalizedPrefix
|
|
51
|
-
self.timestampProvider = timestampProvider ??
|
|
50
|
+
self.timestampProvider = timestampProvider ?? currentTelemetryTimestamp
|
|
52
51
|
self.nowMsProvider = nowMsProvider ?? Self.defaultNowMs
|
|
53
52
|
self.onCaptureError = onCaptureError
|
|
54
53
|
self.dataLoader = dataLoader
|
|
@@ -60,45 +59,63 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
|
|
|
60
59
|
routeTemplate: String? = nil,
|
|
61
60
|
eventID: String? = nil,
|
|
62
61
|
metadata: Metadata? = nil,
|
|
62
|
+
onRequestError: (@Sendable (any Error) throws -> Void)? = nil,
|
|
63
63
|
) async throws -> (Data, URLResponse) {
|
|
64
64
|
let span = try LogBrewTrace.startURLSessionSpan(for: request, routeTemplate: routeTemplate)
|
|
65
65
|
let startedAtMs = nowMsProvider()
|
|
66
66
|
|
|
67
67
|
do {
|
|
68
68
|
let (data, response) = try await dataLoader(span.request)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
span: span,
|
|
72
|
-
response: response,
|
|
73
|
-
durationMs: durationSince(startedAtMs),
|
|
74
|
-
error: nil,
|
|
75
|
-
metadata: metadata,
|
|
76
|
-
))
|
|
69
|
+
let durationMs = durationSince(startedAtMs)
|
|
70
|
+
captureSpan(eventID, span: span, response: response, durationMs: durationMs, metadata: metadata)
|
|
77
71
|
return (data, response)
|
|
78
72
|
} catch {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
response: nil,
|
|
83
|
-
durationMs: durationSince(startedAtMs),
|
|
84
|
-
error: error,
|
|
85
|
-
metadata: metadata,
|
|
86
|
-
))
|
|
73
|
+
let durationMs = durationSince(startedAtMs)
|
|
74
|
+
captureSpan(eventID, span: span, durationMs: durationMs, error: error, metadata: metadata)
|
|
75
|
+
captureRequestError(error, context: span.traceContext, handler: onRequestError)
|
|
87
76
|
throw error
|
|
88
77
|
}
|
|
89
78
|
}
|
|
90
79
|
|
|
91
|
-
private func
|
|
92
|
-
|
|
80
|
+
private func captureRequestError(
|
|
81
|
+
_ requestError: any Error,
|
|
82
|
+
context: LogBrewTraceContext,
|
|
83
|
+
handler: (@Sendable (any Error) throws -> Void)?,
|
|
84
|
+
) {
|
|
85
|
+
guard let handler else {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
capture {
|
|
89
|
+
try LogBrewTrace.withContext(context) {
|
|
90
|
+
try handler(requestError)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func captureSpan(
|
|
96
|
+
_ eventID: String?,
|
|
97
|
+
span: LogBrewURLSessionSpan,
|
|
98
|
+
response: URLResponse? = nil,
|
|
99
|
+
durationMs: Double,
|
|
100
|
+
error: (any Error)? = nil,
|
|
101
|
+
metadata: Metadata?,
|
|
102
|
+
) {
|
|
103
|
+
capture {
|
|
93
104
|
try client.captureURLSessionSpan(
|
|
94
|
-
|
|
105
|
+
eventID ?? nextEventID(),
|
|
95
106
|
timestamp: timestampProvider(),
|
|
96
|
-
span:
|
|
97
|
-
statusCode: (
|
|
98
|
-
durationMs:
|
|
99
|
-
error:
|
|
100
|
-
metadata:
|
|
107
|
+
span: span,
|
|
108
|
+
statusCode: (response as? HTTPURLResponse)?.statusCode,
|
|
109
|
+
durationMs: durationMs,
|
|
110
|
+
error: error,
|
|
111
|
+
metadata: metadata,
|
|
101
112
|
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func capture(_ operation: () throws -> Void) {
|
|
117
|
+
do {
|
|
118
|
+
try operation()
|
|
102
119
|
} catch {
|
|
103
120
|
onCaptureError?(error)
|
|
104
121
|
}
|
|
@@ -113,39 +130,14 @@ public final class LogBrewURLSessionTracer: @unchecked Sendable {
|
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
private func nextEventID() -> String {
|
|
116
|
-
lock.
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
lock.withLock {
|
|
134
|
+
let eventID = "\(eventIDPrefix)_\(nextEventSequence)"
|
|
135
|
+
nextEventSequence += 1
|
|
136
|
+
return eventID
|
|
119
137
|
}
|
|
120
|
-
let eventID = "\(eventIDPrefix)_\(nextEventSequence)"
|
|
121
|
-
nextEventSequence += 1
|
|
122
|
-
return eventID
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private static func defaultTimestamp() -> String {
|
|
126
|
-
let formatter = ISO8601DateFormatter()
|
|
127
|
-
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
128
|
-
return formatter.string(from: Date())
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
private static func defaultNowMs() -> Double {
|
|
132
141
|
ProcessInfo.processInfo.systemUptime * 1000
|
|
133
142
|
}
|
|
134
143
|
}
|
|
135
|
-
|
|
136
|
-
private struct URLSessionTraceCapture {
|
|
137
|
-
let eventID: String?
|
|
138
|
-
let span: LogBrewURLSessionSpan
|
|
139
|
-
let response: URLResponse?
|
|
140
|
-
let durationMs: Double
|
|
141
|
-
let error: (any Error)?
|
|
142
|
-
let metadata: Metadata?
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private struct LogBrewURLSessionDataLoader: @unchecked Sendable {
|
|
146
|
-
let session: URLSession
|
|
147
|
-
|
|
148
|
-
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
|
149
|
-
try await session.data(for: request)
|
|
150
|
-
}
|
|
151
|
-
}
|