@logbrew/react-native 0.1.23 → 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 (35) hide show
  1. package/README.md +62 -5
  2. package/android/CMakeLists.txt +7 -0
  3. package/android/build.gradle +12 -0
  4. package/android/src/main/cpp/android_diagnostics.cpp +363 -0
  5. package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
  6. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
  7. package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
  8. package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
  9. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
  10. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +111 -156
  11. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
  12. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
  13. package/android-native-diagnostics.d.ts +29 -0
  14. package/android-native-diagnostics.js +176 -0
  15. package/fatal-replay.cjs +35 -3
  16. package/global-errors.d.cts +3 -3
  17. package/global-errors.d.ts +3 -3
  18. package/global-errors.native.js +1 -12
  19. package/index.cjs +1 -1
  20. package/index.native.d.ts +4 -0
  21. package/index.native.js +21 -3
  22. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
  28. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
  29. package/ios/LBRNFatalStoreModule.mm +15 -86
  30. package/package.json +17 -5
  31. package/persistent-delivery.native.js +4 -3
  32. package/src/NativeLogBrewFatalStore.ts +5 -4
  33. package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
  34. package/ios/LBRNFatalRecordStore.h +0 -25
  35. package/ios/LBRNFatalRecordStore.m +0 -542
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  React Native helpers for the public LogBrew JavaScript SDK.
8
8
 
9
- This package keeps event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`. It adds mobile helpers for screen views, app-state changes, product actions, API milestones, bounded issue breadcrumbs, typed handled and unhandled JavaScript errors, Promise rejection tracking, provider and hook usage, W3C trace correlation and propagation, lifecycle spans, resource spans, reversible instrumentation, and opt-in Apple native crash and app-hang diagnostics.
9
+ This package keeps event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`. It adds mobile helpers for screen views, app-state changes, product actions, API milestones, bounded issue breadcrumbs, typed handled and unhandled JavaScript errors, Promise rejection tracking, provider and hook usage, W3C trace correlation and propagation, lifecycle spans, resource spans, reversible instrumentation, and opt-in native crash and app-hang diagnostics.
10
10
 
11
11
  ## Install
12
12
 
@@ -267,16 +267,73 @@ errorHandler.remove();
267
267
 
268
268
  Installation is idempotent for the active React Native `ErrorUtils` object. The wrapper captures a fixed-content, path-bounded issue for nonfatal global JavaScript errors and then calls the handler that was installed before it. Capture and diagnostic callback failures cannot prevent that prior handler from running. `remove()` reinstates the previous handler only while LogBrew still owns the global slot, so a later integration is not overwritten.
269
269
 
270
- The React Native conditional export obtains LogBrew's synchronous native fatal store through the supported TurboModule or `NativeModules` seam. Before chaining a fatal report, it writes one bounded record to app-private storage that is excluded from operating-system archives. On a later installation it performs stable-ID at-least-once replay, and acknowledgement happens only after local queue admission is observable through the SDK queue counters. Filtered, dropped, unknown-admission, persistence-failed, and acknowledgement-failed records are retained. A failed acknowledgement is retried without admitting the same ID twice in one JavaScript runtime. Use `fatalHealth()` for frozen bounded counters and status, or `discardPendingFatalRecord()` for an explicit rollback discard. The Node ESM and CommonJS entries never import React Native; non-React-Native callers must inject `fatalStore` explicitly.
271
-
272
- Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This error-handler integration does not claim mathematically exactly-once delivery, native crash capture, ANR or hang detection, general offline queueing by the fatal-record slot, or symbolication. The client-level persistent queue above owns normal event restart delivery.
270
+ The React Native conditional export admits fatal JavaScript reports directly
271
+ into the same app-private persistent queue used by normal delivery, then calls
272
+ the previous handler. Queue admission is synchronous and must increase the
273
+ pending count without increasing the dropped count. Otherwise capture fails
274
+ closed and reports a fixed diagnostic code. The next process provides
275
+ stable-ID at-least-once replay. Its acknowledgement happens only after local queue admission
276
+ is observable through the SDK queue counters. Normal batch acknowledgement
277
+ removes the event only after accepted delivery. Use
278
+ `persistentQueue: "required"` when fatal
279
+ survival is required. `fatalHealth()` exposes frozen bounded counters and
280
+ status. The Node ESM and CommonJS entries never import React Native;
281
+ non-React-Native callers may still inject the documented advanced `fatalStore`
282
+ adapter.
283
+
284
+ Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This error-handler integration does not claim mathematically exactly-once delivery, native crash capture, ANR or hang detection, or symbolication. The client-level persistent queue owns restart delivery.
273
285
 
274
286
  ### Apple native crash and app-hang diagnostics
275
287
 
276
288
  Apple native diagnostics are an iOS-only, opt-in build feature. They capture a
277
289
  fatal native process crash and replay one privacy-bounded issue after the next
278
290
  launch. Set `hangThresholdSeconds` to also capture recovered or ongoing UIKit
279
- main-thread hangs. Android native crash and ANR capture are not included.
291
+ main-thread hangs.
292
+
293
+ ### Android native crash and ANR diagnostics
294
+
295
+ Android diagnostics are opt-in and require a native build. They persist Java
296
+ crashes, one NDK fault frame, and main-thread hangs to the normal app-private
297
+ event queue. NDK evidence enters that queue on the next launch and remains
298
+ until queue admission succeeds. Native signal records retain the project,
299
+ release, environment, and service active at crash time, so an app update before
300
+ relaunch cannot reassign the crash to a newer build.
301
+
302
+ Remove other Android fatal handlers, then install before creating the client or
303
+ registering the React root:
304
+
305
+ ```js
306
+ import {
307
+ createLogBrewReactNativeClient,
308
+ installLogBrewAndroidNativeDiagnostics
309
+ } from "@logbrew/react-native";
310
+
311
+ installLogBrewAndroidNativeDiagnostics({
312
+ clientKey: process.env.EXPO_PUBLIC_LOGBREW_CLIENT_KEY,
313
+ projectId: "550e8400-e29b-41d4-a716-446655440000",
314
+ release: "com.example.app@1.2.3+45",
315
+ environment: "production",
316
+ service: "android-app",
317
+ fatalHandlerOwnership: "logbrew",
318
+ anrThresholdMs: 5000
319
+ });
320
+
321
+ const client = createLogBrewReactNativeClient({
322
+ clientKey: process.env.EXPO_PUBLIC_LOGBREW_CLIENT_KEY,
323
+ persistentQueue: "required"
324
+ });
325
+ ```
326
+
327
+ Capture excludes messages, thread names, console output, local variables,
328
+ request data, authentication values, device identifiers, and raw tombstones. Java and
329
+ hang reports keep at most 32 frames; NDK reports keep one build ID and offset.
330
+
331
+ `uninstallLogBrewAndroidNativeDiagnostics()` reinstates prior
332
+ handlers, stops the watchdog, and leaves queued events available. After closing
333
+ the client, purge only for an explicit discard. Upload exact-release Android
334
+ symbols with `logbrew debug-artifacts upload`, require build-ID/architecture
335
+ lookup, and preserve the old integration until the target app proves crash
336
+ replay, hang capture, and symbolicated hosted readback.
280
337
 
281
338
  For Expo prebuild or development builds, add the config plugin:
282
339
 
@@ -0,0 +1,7 @@
1
+ cmake_minimum_required(VERSION 3.22.1)
2
+ project(logbrew_android_diagnostics LANGUAGES CXX)
3
+
4
+ add_library(logbrew_android_diagnostics SHARED src/main/cpp/android_diagnostics.cpp)
5
+ target_compile_features(logbrew_android_diagnostics PRIVATE cxx_std_17)
6
+ target_compile_options(logbrew_android_diagnostics PRIVATE -Wall -Wextra -Werror -fno-exceptions -fno-rtti)
7
+ target_link_libraries(logbrew_android_diagnostics PRIVATE dl)
@@ -26,6 +26,18 @@ android {
26
26
  targetSdkVersion rootProject.ext.has("targetSdkVersion")
27
27
  ? rootProject.ext.get("targetSdkVersion")
28
28
  : 35
29
+ externalNativeBuild {
30
+ cmake {
31
+ cppFlags "-std=c++17"
32
+ }
33
+ }
34
+ }
35
+
36
+ externalNativeBuild {
37
+ cmake {
38
+ path "CMakeLists.txt"
39
+ version "3.22.1"
40
+ }
29
41
  }
30
42
 
31
43
  sourceSets {
@@ -0,0 +1,363 @@
1
+ #include <jni.h>
2
+
3
+ #include <array>
4
+ #include <atomic>
5
+ #include <cstdint>
6
+ #include <cstdio>
7
+ #include <cstring>
8
+ #include <ctime>
9
+ #include <dlfcn.h>
10
+ #include <elf.h>
11
+ #include <fcntl.h>
12
+ #include <link.h>
13
+ #include <signal.h>
14
+ #include <sys/syscall.h>
15
+ #include <unistd.h>
16
+ #include <ucontext.h>
17
+
18
+ namespace {
19
+ constexpr uint32_t kMagic = 0x4c424e53;
20
+ constexpr uint32_t kVersion = 1;
21
+ constexpr size_t kIdBytes = 96;
22
+ constexpr size_t kUuidBytes = 36;
23
+ constexpr size_t kArchBytes = 16;
24
+ constexpr size_t kOffsetBytes = 16;
25
+ constexpr size_t kProjectChars = 36;
26
+ constexpr size_t kReleaseChars = 256;
27
+ constexpr size_t kEnvironmentChars = 128;
28
+ constexpr size_t kServiceChars = 128;
29
+ constexpr size_t kMaxModules = 192;
30
+ constexpr std::array<int, 6> kSignals = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP};
31
+
32
+ #pragma pack(push, 1)
33
+ struct SignalRecord {
34
+ uint32_t magic;
35
+ uint32_t version;
36
+ uint32_t signal;
37
+ uint64_t timestamp_ms;
38
+ char event_id[kIdBytes];
39
+ char image_uuid[kUuidBytes];
40
+ char architecture[kArchBytes];
41
+ char instruction_offset[kOffsetBytes];
42
+ uint16_t project_id[kProjectChars];
43
+ uint16_t release[kReleaseChars];
44
+ uint16_t environment[kEnvironmentChars];
45
+ uint16_t service[kServiceChars];
46
+ uint32_t checksum;
47
+ };
48
+ #pragma pack(pop)
49
+ static_assert(sizeof(SignalRecord) == 1284, "signal record layout drifted");
50
+
51
+ struct Module {
52
+ uintptr_t start;
53
+ uintptr_t end;
54
+ std::array<uint8_t, 16> build_id;
55
+ bool has_build_id;
56
+ };
57
+
58
+ std::array<Module, kMaxModules> g_modules{};
59
+ size_t g_module_count = 0;
60
+ std::array<struct sigaction, kSignals.size()> g_previous_actions{};
61
+ std::array<char, 1024> g_record_path{};
62
+ SignalRecord g_record_template{};
63
+ std::atomic<bool> g_installed{false};
64
+ std::atomic_flag g_handling = ATOMIC_FLAG_INIT;
65
+
66
+ uint32_t crc32(const uint8_t* bytes, size_t length) {
67
+ uint32_t value = 0xffffffffU;
68
+ for (size_t index = 0; index < length; ++index) {
69
+ value ^= bytes[index];
70
+ for (int bit = 0; bit < 8; ++bit) {
71
+ value = (value >> 1U) ^ (0xedb88320U & (0U - (value & 1U)));
72
+ }
73
+ }
74
+ return value ^ 0xffffffffU;
75
+ }
76
+
77
+ size_t align4(size_t value) {
78
+ return (value + 3U) & ~size_t{3U};
79
+ }
80
+
81
+ bool read_build_id(const dl_phdr_info* info, std::array<uint8_t, 16>* output) {
82
+ for (ElfW(Half) index = 0; index < info->dlpi_phnum; ++index) {
83
+ const ElfW(Phdr)& header = info->dlpi_phdr[index];
84
+ if (header.p_type != PT_NOTE || header.p_memsz < sizeof(ElfW(Nhdr))) {
85
+ continue;
86
+ }
87
+ const auto* cursor = reinterpret_cast<const uint8_t*>(info->dlpi_addr + header.p_vaddr);
88
+ const auto* end = cursor + header.p_memsz;
89
+ while (cursor + sizeof(ElfW(Nhdr)) <= end) {
90
+ const auto* note = reinterpret_cast<const ElfW(Nhdr)*>(cursor);
91
+ cursor += sizeof(ElfW(Nhdr));
92
+ const size_t name_bytes = align4(note->n_namesz);
93
+ const size_t value_bytes = align4(note->n_descsz);
94
+ if (cursor + name_bytes + value_bytes > end) {
95
+ break;
96
+ }
97
+ const uint8_t* name = cursor;
98
+ const uint8_t* value = cursor + name_bytes;
99
+ if (note->n_type == NT_GNU_BUILD_ID
100
+ && note->n_namesz >= 3
101
+ && std::memcmp(name, "GNU", 3) == 0
102
+ && note->n_descsz >= output->size()) {
103
+ std::memcpy(output->data(), value, output->size());
104
+ return true;
105
+ }
106
+ cursor += name_bytes + value_bytes;
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+
112
+ int collect_module(dl_phdr_info* info, size_t, void*) {
113
+ if (g_module_count == g_modules.size()) {
114
+ return 1;
115
+ }
116
+ uintptr_t start = UINTPTR_MAX;
117
+ uintptr_t end = 0;
118
+ for (ElfW(Half) index = 0; index < info->dlpi_phnum; ++index) {
119
+ const ElfW(Phdr)& header = info->dlpi_phdr[index];
120
+ if (header.p_type != PT_LOAD) {
121
+ continue;
122
+ }
123
+ const uintptr_t segment_start = info->dlpi_addr + header.p_vaddr;
124
+ start = segment_start < start ? segment_start : start;
125
+ const uintptr_t segment_end = segment_start + header.p_memsz;
126
+ end = segment_end > end ? segment_end : end;
127
+ }
128
+ if (start == UINTPTR_MAX || end <= start) {
129
+ return 0;
130
+ }
131
+ Module module{};
132
+ module.start = start;
133
+ module.end = end;
134
+ module.has_build_id = read_build_id(info, &module.build_id);
135
+ g_modules[g_module_count++] = module;
136
+ return 0;
137
+ }
138
+
139
+ uintptr_t program_counter(void* context) {
140
+ const auto* machine = reinterpret_cast<const ucontext_t*>(context);
141
+ #if defined(__aarch64__)
142
+ return machine->uc_mcontext.pc;
143
+ #elif defined(__arm__)
144
+ return machine->uc_mcontext.arm_pc;
145
+ #elif defined(__x86_64__)
146
+ return machine->uc_mcontext.gregs[REG_RIP];
147
+ #elif defined(__i386__)
148
+ return machine->uc_mcontext.gregs[REG_EIP];
149
+ #else
150
+ return 0;
151
+ #endif
152
+ }
153
+
154
+ const char* architecture() {
155
+ #if defined(__aarch64__)
156
+ return "arm64";
157
+ #elif defined(__arm__)
158
+ return "arm";
159
+ #elif defined(__x86_64__)
160
+ return "x86_64";
161
+ #elif defined(__i386__)
162
+ return "x86";
163
+ #else
164
+ return "";
165
+ #endif
166
+ }
167
+
168
+ const Module* module_for(uintptr_t address) {
169
+ for (size_t index = 0; index < g_module_count; ++index) {
170
+ const Module& module = g_modules[index];
171
+ if (module.has_build_id && address >= module.start && address < module.end) {
172
+ return &module;
173
+ }
174
+ }
175
+ return nullptr;
176
+ }
177
+
178
+ void uuid_string(const std::array<uint8_t, 16>& bytes, char* output) {
179
+ static constexpr char hex[] = "0123456789abcdef";
180
+ size_t destination = 0;
181
+ for (size_t index = 0; index < bytes.size(); ++index) {
182
+ if (index == 4 || index == 6 || index == 8 || index == 10) {
183
+ output[destination++] = '-';
184
+ }
185
+ output[destination++] = hex[bytes[index] >> 4U];
186
+ output[destination++] = hex[bytes[index] & 0x0fU];
187
+ }
188
+ }
189
+
190
+ void offset_string(uintptr_t value, char* output) {
191
+ static constexpr char hex[] = "0123456789abcdef";
192
+ for (int index = 15; index >= 0; --index) {
193
+ output[index] = hex[value & 0x0fU];
194
+ value >>= 4U;
195
+ }
196
+ }
197
+
198
+ void write_record(int signal_number, void* context) {
199
+ const uintptr_t address = program_counter(context);
200
+ const Module* module = module_for(address);
201
+ if (module == nullptr || address < module->start) {
202
+ return;
203
+ }
204
+ SignalRecord record = g_record_template;
205
+ record.signal = static_cast<uint32_t>(signal_number);
206
+ timespec now{};
207
+ if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
208
+ return;
209
+ }
210
+ record.timestamp_ms = static_cast<uint64_t>(now.tv_sec) * 1000U
211
+ + static_cast<uint64_t>(now.tv_nsec / 1000000U);
212
+ uuid_string(module->build_id, record.image_uuid);
213
+ const char* arch = architecture();
214
+ size_t arch_length = 0;
215
+ while (arch[arch_length] != '\0') {
216
+ ++arch_length;
217
+ }
218
+ std::memcpy(record.architecture, arch, arch_length);
219
+ offset_string(address - module->start, record.instruction_offset);
220
+ record.checksum = crc32(reinterpret_cast<const uint8_t*>(&record), sizeof(record) - 4);
221
+
222
+ const int descriptor = open(
223
+ g_record_path.data(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600);
224
+ if (descriptor < 0) {
225
+ return;
226
+ }
227
+ const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&record);
228
+ size_t written = 0;
229
+ while (written < sizeof(record)) {
230
+ const ssize_t count = write(descriptor, bytes + written, sizeof(record) - written);
231
+ if (count <= 0) {
232
+ break;
233
+ }
234
+ written += static_cast<size_t>(count);
235
+ }
236
+ if (written == sizeof(record)) {
237
+ fsync(descriptor);
238
+ }
239
+ close(descriptor);
240
+ }
241
+
242
+ void chain_signal(int signal_number) {
243
+ for (size_t index = 0; index < kSignals.size(); ++index) {
244
+ if (kSignals[index] == signal_number) {
245
+ sigaction(signal_number, &g_previous_actions[index], nullptr);
246
+ break;
247
+ }
248
+ }
249
+ syscall(SYS_tgkill, getpid(), gettid(), signal_number);
250
+ }
251
+
252
+ void signal_handler(int signal_number, siginfo_t*, void* context) {
253
+ if (!g_handling.test_and_set()) {
254
+ write_record(signal_number, context);
255
+ }
256
+ chain_signal(signal_number);
257
+ }
258
+
259
+ bool copy_jstring(JNIEnv* environment, jstring value, char* output, size_t capacity) {
260
+ if (value == nullptr) {
261
+ return false;
262
+ }
263
+ const char* bytes = environment->GetStringUTFChars(value, nullptr);
264
+ if (bytes == nullptr) {
265
+ return false;
266
+ }
267
+ const size_t length = std::strlen(bytes);
268
+ const bool valid = length > 0 && length < capacity;
269
+ if (valid) {
270
+ std::memset(output, 0, capacity);
271
+ std::memcpy(output, bytes, length);
272
+ }
273
+ environment->ReleaseStringUTFChars(value, bytes);
274
+ return valid;
275
+ }
276
+
277
+ bool copy_jstring_utf16(
278
+ JNIEnv* environment, jstring value, uint16_t* output, size_t capacity) {
279
+ if (value == nullptr) {
280
+ return false;
281
+ }
282
+ const jsize length = environment->GetStringLength(value);
283
+ if (length <= 0 || static_cast<size_t>(length) > capacity) {
284
+ return false;
285
+ }
286
+ std::memset(output, 0, capacity * sizeof(uint16_t));
287
+ environment->GetStringRegion(value, 0, length, reinterpret_cast<jchar*>(output));
288
+ return !environment->ExceptionCheck();
289
+ }
290
+ } // namespace
291
+
292
+ extern "C" JNIEXPORT jboolean JNICALL
293
+ Java_co_logbrew_reactnative_AndroidDiagnosticsRuntime_nativeInstall(
294
+ JNIEnv* environment,
295
+ jclass,
296
+ jstring record_path,
297
+ jstring event_id,
298
+ jstring project_id,
299
+ jstring release,
300
+ jstring deployment_environment,
301
+ jstring service) {
302
+ bool expected = false;
303
+ if (!g_installed.compare_exchange_strong(expected, true)) {
304
+ return JNI_FALSE;
305
+ }
306
+ if (!copy_jstring(environment, record_path, g_record_path.data(), g_record_path.size())
307
+ || !copy_jstring(
308
+ environment,
309
+ event_id,
310
+ g_record_template.event_id,
311
+ sizeof(g_record_template.event_id))
312
+ || !copy_jstring_utf16(
313
+ environment,
314
+ project_id,
315
+ g_record_template.project_id,
316
+ kProjectChars)
317
+ || !copy_jstring_utf16(
318
+ environment, release, g_record_template.release, kReleaseChars)
319
+ || !copy_jstring_utf16(
320
+ environment,
321
+ deployment_environment,
322
+ g_record_template.environment,
323
+ kEnvironmentChars)
324
+ || !copy_jstring_utf16(
325
+ environment, service, g_record_template.service, kServiceChars)) {
326
+ g_installed.store(false);
327
+ return JNI_FALSE;
328
+ }
329
+ g_record_template.magic = kMagic;
330
+ g_record_template.version = kVersion;
331
+ g_module_count = 0;
332
+ dl_iterate_phdr(collect_module, nullptr);
333
+ struct sigaction action {};
334
+ sigemptyset(&action.sa_mask);
335
+ action.sa_sigaction = signal_handler;
336
+ action.sa_flags = SA_SIGINFO | SA_ONSTACK;
337
+ for (size_t index = 0; index < kSignals.size(); ++index) {
338
+ if (sigaction(kSignals[index], &action, &g_previous_actions[index]) != 0) {
339
+ for (size_t rollback = 0; rollback < index; ++rollback) {
340
+ sigaction(kSignals[rollback], &g_previous_actions[rollback], nullptr);
341
+ }
342
+ g_installed.store(false);
343
+ return JNI_FALSE;
344
+ }
345
+ }
346
+ return JNI_TRUE;
347
+ }
348
+
349
+ extern "C" JNIEXPORT void JNICALL
350
+ Java_co_logbrew_reactnative_AndroidDiagnosticsRuntime_nativeUninstall(JNIEnv*, jclass) {
351
+ if (!g_installed.exchange(false)) {
352
+ return;
353
+ }
354
+ for (size_t index = 0; index < kSignals.size(); ++index) {
355
+ struct sigaction current {};
356
+ if (sigaction(kSignals[index], nullptr, &current) == 0
357
+ && (current.sa_flags & SA_SIGINFO) != 0
358
+ && current.sa_sigaction == signal_handler) {
359
+ sigaction(kSignals[index], &g_previous_actions[index], nullptr);
360
+ }
361
+ }
362
+ g_handling.clear();
363
+ }
@@ -0,0 +1,214 @@
1
+ package co.logbrew.reactnative;
2
+
3
+ import android.app.ActivityManager;
4
+ import android.content.Context;
5
+ import android.os.Handler;
6
+ import android.os.Looper;
7
+ import android.os.SystemClock;
8
+ import java.io.File;
9
+ import java.util.concurrent.Executors;
10
+ import java.util.concurrent.ScheduledExecutorService;
11
+ import java.util.concurrent.TimeUnit;
12
+ import java.util.concurrent.atomic.AtomicBoolean;
13
+ import java.util.concurrent.atomic.AtomicLong;
14
+ import java.util.function.LongConsumer;
15
+
16
+ final class AndroidDiagnosticsRuntime implements Thread.UncaughtExceptionHandler {
17
+ private static final boolean SIGNAL_CAPTURE_AVAILABLE = loadSignalCapture();
18
+
19
+ private final AndroidNativeDiagnostics diagnostics;
20
+ private final Thread.UncaughtExceptionHandler previousHandler;
21
+ private final AndroidNativeDiagnostics.Configuration configuration;
22
+ private final String signalRecordPath;
23
+
24
+ AndroidDiagnosticsRuntime(
25
+ Context context,
26
+ File storageRoot,
27
+ EventRecordStore eventStore,
28
+ AndroidNativeDiagnostics.Configuration configuration) {
29
+ this.configuration = configuration;
30
+ previousHandler = Thread.getDefaultUncaughtExceptionHandler();
31
+ MainThreadScheduler scheduler = new MainThreadScheduler(context);
32
+ AndroidNativeSignalStore signalStore =
33
+ new AndroidNativeSignalStore(new File(storageRoot, "logbrew-android-native-v1.signal"));
34
+ diagnostics =
35
+ new AndroidNativeDiagnostics(
36
+ eventStore,
37
+ signalStore,
38
+ configuration,
39
+ scheduler,
40
+ () -> {},
41
+ this::chain,
42
+ scheduler.mainThread::getStackTrace);
43
+ signalRecordPath = signalStore.path();
44
+ }
45
+
46
+ synchronized String install() {
47
+ if (diagnostics.installed) {
48
+ return "already_installed";
49
+ }
50
+ if (pending() < 0) {
51
+ throw new IllegalStateException("event queue unavailable");
52
+ }
53
+ String status = diagnostics.install();
54
+ Thread.setDefaultUncaughtExceptionHandler(this);
55
+ if (!diagnostics.signalStore.prepare(new AndroidParentDirectorySync())
56
+ || !SIGNAL_CAPTURE_AVAILABLE
57
+ || !nativeInstall(
58
+ signalRecordPath,
59
+ diagnostics.nextNativeEventId(),
60
+ configuration.projectId,
61
+ configuration.release,
62
+ configuration.environment,
63
+ configuration.service)) {
64
+ Thread.setDefaultUncaughtExceptionHandler(previousHandler);
65
+ diagnostics.uninstall();
66
+ throw new IllegalStateException("signal capture unavailable");
67
+ }
68
+ return status;
69
+ }
70
+
71
+ synchronized String uninstall() {
72
+ if (!diagnostics.installed) {
73
+ return "not_installed";
74
+ }
75
+ if (SIGNAL_CAPTURE_AVAILABLE) {
76
+ nativeUninstall();
77
+ }
78
+ if (Thread.getDefaultUncaughtExceptionHandler() == this) {
79
+ Thread.setDefaultUncaughtExceptionHandler(previousHandler);
80
+ }
81
+ diagnostics.uninstall();
82
+ return "uninstalled";
83
+ }
84
+
85
+ synchronized int pending() {
86
+ EventRecordStore.Result result = diagnostics.eventStore.load();
87
+ return "loaded".equals(result.status) ? result.records.size() : -1;
88
+ }
89
+
90
+ synchronized boolean installed() {
91
+ return diagnostics.installed;
92
+ }
93
+
94
+ synchronized boolean matches(
95
+ EventRecordStore eventStore, AndroidNativeDiagnostics.Configuration configuration) {
96
+ return diagnostics.eventStore == eventStore && diagnostics.configurationMatches(configuration);
97
+ }
98
+
99
+ @Override
100
+ public void uncaughtException(Thread thread, Throwable error) {
101
+ diagnostics.handleUncaught(thread, error);
102
+ }
103
+
104
+ private void chain(Thread thread, Throwable error) {
105
+ if (previousHandler != null && previousHandler != this) {
106
+ previousHandler.uncaughtException(thread, error);
107
+ return;
108
+ }
109
+ android.os.Process.killProcess(android.os.Process.myPid());
110
+ System.exit(10);
111
+ }
112
+
113
+ private static boolean loadSignalCapture() {
114
+ try {
115
+ System.loadLibrary("logbrew_android_diagnostics");
116
+ return true;
117
+ } catch (LinkageError error) {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ private static native boolean nativeInstall(
123
+ String recordPath,
124
+ String eventId,
125
+ String projectId,
126
+ String release,
127
+ String environment,
128
+ String service);
129
+
130
+ private static native void nativeUninstall();
131
+
132
+ private static final class MainThreadScheduler implements AndroidNativeDiagnostics.Scheduler {
133
+ private final Context context;
134
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
135
+ private final Thread mainThread = Looper.getMainLooper().getThread();
136
+ private final AtomicLong heartbeatMs = new AtomicLong(SystemClock.elapsedRealtime());
137
+ private final AtomicBoolean reported = new AtomicBoolean();
138
+ private ScheduledExecutorService executor;
139
+
140
+ MainThreadScheduler(Context context) {
141
+ this.context = context.getApplicationContext();
142
+ }
143
+
144
+ @Override
145
+ public synchronized void start(
146
+ long thresholdMs, LongConsumer report) {
147
+ stop();
148
+ heartbeatMs.set(SystemClock.elapsedRealtime());
149
+ mainHandler.post(this::heartbeat);
150
+ executor =
151
+ Executors.newSingleThreadScheduledExecutor(
152
+ runnable -> {
153
+ Thread thread = new Thread(runnable, "LogBrew-ANR-Watchdog");
154
+ thread.setDaemon(true);
155
+ return thread;
156
+ });
157
+ long intervalMs = Math.max(500, thresholdMs / 2);
158
+ executor.scheduleWithFixedDelay(
159
+ () -> {
160
+ long now = SystemClock.elapsedRealtime();
161
+ long elapsedMs = now - heartbeatMs.get();
162
+ boolean debugging = android.os.Debug.isDebuggerConnected()
163
+ || android.os.Debug.waitingForDebugger();
164
+ if (AndroidNativeDiagnostics.shouldReportHang(
165
+ elapsedMs, thresholdMs, debugging, processNotResponding())
166
+ && reported.compareAndSet(false, true)) {
167
+ report.accept(elapsedMs);
168
+ }
169
+ mainHandler.post(this::heartbeat);
170
+ },
171
+ thresholdMs,
172
+ intervalMs,
173
+ TimeUnit.MILLISECONDS);
174
+ }
175
+
176
+ @Override
177
+ public synchronized void stop() {
178
+ if (executor != null) {
179
+ executor.shutdownNow();
180
+ executor = null;
181
+ }
182
+ }
183
+
184
+ private void heartbeat() {
185
+ heartbeatMs.set(SystemClock.elapsedRealtime());
186
+ reported.set(false);
187
+ }
188
+
189
+ private boolean processNotResponding() {
190
+ ActivityManager manager =
191
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
192
+ if (manager == null) {
193
+ return false;
194
+ }
195
+ try {
196
+ java.util.List<ActivityManager.ProcessErrorStateInfo> states =
197
+ manager.getProcessesInErrorState();
198
+ if (states != null) {
199
+ int processId = android.os.Process.myPid();
200
+ for (ActivityManager.ProcessErrorStateInfo state : states) {
201
+ if (state != null
202
+ && state.pid == processId
203
+ && state.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
204
+ return true;
205
+ }
206
+ }
207
+ }
208
+ } catch (RuntimeException ignored) {
209
+ return false;
210
+ }
211
+ return false;
212
+ }
213
+ }
214
+ }