@react-native-firebase/crashlytics 26.4.0 → 27.1.0

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 (36) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/RNFBCrashlytics.podspec +7 -1
  3. package/android/build.gradle +8 -0
  4. package/android/proguard-rules.pro +20 -0
  5. package/android/src/main/java/io/invertase/firebase/crashlytics/Constants.kt +42 -0
  6. package/android/src/main/java/io/invertase/firebase/crashlytics/{JavaScriptError.java → JavaScriptError.kt} +4 -6
  7. package/android/src/main/java/io/invertase/firebase/crashlytics/NativeRNFBTurboCrashlytics.java +11 -4
  8. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExit.java +117 -0
  9. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternal.java +80 -0
  10. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelper.kt +45 -0
  11. package/android/src/main/java/io/invertase/firebase/crashlytics/{UnhandledPromiseRejection.java → UnhandledPromiseRejection.kt} +4 -6
  12. package/android/src/test/java/io/invertase/firebase/crashlytics/ConstantsAndJsErrorTypesTest.java +84 -0
  13. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitBarrierTest.java +122 -0
  14. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitTest.java +189 -0
  15. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternalTest.java +199 -0
  16. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelperTest.java +108 -0
  17. package/dist/module/version.js +1 -1
  18. package/dist/typescript/lib/index.d.ts +1 -1
  19. package/dist/typescript/lib/version.d.ts +1 -1
  20. package/ios/RNFBCrashlytics/RNFBCrashlyticsDebuggerProbe.swift +121 -0
  21. package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.h +98 -0
  22. package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.m +182 -0
  23. package/ios/RNFBCrashlytics/RNFBCrashlyticsModule.mm +57 -75
  24. package/ios/RNFBCrashlytics.xcodeproj/project.pbxproj +14 -0
  25. package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/FIRCrashlyticsHostStub.m +273 -0
  26. package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/Firebase.h +56 -0
  27. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsDebuggerProbeTests.swift +112 -0
  28. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsFatalExitTests.m +268 -0
  29. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsHelperHostStubTests.m +119 -0
  30. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/project.pbxproj +265 -0
  31. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBCrashlyticsUnitTests.xcscheme +69 -0
  32. package/ios_config.sh +38 -5
  33. package/lib/version.ts +1 -1
  34. package/package.json +4 -4
  35. package/android/src/main/java/io/invertase/firebase/crashlytics/Constants.java +0 -29
  36. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelper.java +0 -18
@@ -0,0 +1,189 @@
1
+ package io.invertase.firebase.crashlytics;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import static org.junit.Assert.assertEquals;
21
+ import static org.junit.Assert.assertFalse;
22
+ import static org.junit.Assert.assertTrue;
23
+
24
+ import java.util.ArrayList;
25
+ import java.util.Arrays;
26
+ import java.util.Collections;
27
+ import java.util.List;
28
+ import java.util.concurrent.TimeoutException;
29
+ import org.junit.Test;
30
+
31
+ /**
32
+ * Plain JUnit (AndroidTest-AD-1) for the JS-fatal record → barrier → terminate ordering. A fake
33
+ * clock/sleeper/terminator stand in for time and {@code System.exit}, so no test ends the JVM.
34
+ */
35
+ public class ReactNativeFirebaseCrashlyticsFatalExitTest {
36
+ private static final long TIMEOUT = 2000;
37
+
38
+ private final List<String> events = Collections.synchronizedList(new ArrayList<>());
39
+ private final List<Long> sleeps = new ArrayList<>();
40
+ private long now = 10_000;
41
+
42
+ private ReactNativeFirebaseCrashlyticsFatalExit fatalExit(
43
+ ReactNativeFirebaseCrashlyticsFatalExit.Barrier barrier,
44
+ ReactNativeFirebaseCrashlyticsFatalExit.Sleeper sleeper) {
45
+ return new ReactNativeFirebaseCrashlyticsFatalExit(
46
+ barrier, sleeper, () -> now, () -> events.add("exit"), TIMEOUT);
47
+ }
48
+
49
+ private ReactNativeFirebaseCrashlyticsFatalExit.Sleeper advancingSleeper() {
50
+ return millis -> {
51
+ sleeps.add(millis);
52
+ now += millis;
53
+ };
54
+ }
55
+
56
+ @Test
57
+ public void barrierSuccess_terminatesAfterRecordAndBarrierWithoutWaiting() {
58
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
59
+ fatalExit(
60
+ timeoutMs -> {
61
+ assertEquals(TIMEOUT, timeoutMs);
62
+ events.add("barrier");
63
+ },
64
+ advancingSleeper());
65
+
66
+ assertTrue(fatalExit.recordThenTerminate(() -> events.add("record")));
67
+
68
+ assertEquals(Arrays.asList("record", "barrier", "exit"), events);
69
+ assertTrue(sleeps.isEmpty());
70
+ }
71
+
72
+ @Test
73
+ public void recordThrows_stillWaitsOnBarrierAndTerminates() {
74
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
75
+ fatalExit(timeoutMs -> events.add("barrier"), advancingSleeper());
76
+
77
+ assertTrue(
78
+ fatalExit.recordThenTerminate(
79
+ () -> {
80
+ events.add("record");
81
+ throw new IllegalStateException("record failed");
82
+ }));
83
+
84
+ assertEquals(Arrays.asList("record", "barrier", "exit"), events);
85
+ assertFalse(fatalExit.recordThenTerminate(() -> events.add("second")));
86
+ assertEquals(Arrays.asList("record", "barrier", "exit"), events);
87
+ }
88
+
89
+ @Test
90
+ public void barrierTimeout_terminatesOnceTheBoundHasElapsed() {
91
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
92
+ fatalExit(
93
+ timeoutMs -> {
94
+ now += timeoutMs;
95
+ throw new TimeoutException("barrier timed out");
96
+ },
97
+ advancingSleeper());
98
+
99
+ assertTrue(fatalExit.recordThenTerminate(() -> events.add("record")));
100
+
101
+ assertEquals(Arrays.asList("record", "exit"), events);
102
+ assertTrue("bound already used up by the barrier", sleeps.isEmpty());
103
+ }
104
+
105
+ @Test
106
+ public void reflectionFailure_waitsOutTheRemainingBoundThenTerminates() {
107
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
108
+ fatalExit(
109
+ timeoutMs -> {
110
+ now += 300;
111
+ throw new NoSuchFieldException("crashlyticsWorkers");
112
+ },
113
+ advancingSleeper());
114
+
115
+ assertTrue(fatalExit.recordThenTerminate(() -> events.add("record")));
116
+
117
+ assertEquals(Collections.singletonList(TIMEOUT - 300), sleeps);
118
+ assertEquals(Arrays.asList("record", "exit"), events);
119
+ }
120
+
121
+ @Test
122
+ public void linkageError_fallsBackToTheBoundedWait() {
123
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
124
+ fatalExit(
125
+ timeoutMs -> {
126
+ throw new NoSuchFieldError("core");
127
+ },
128
+ advancingSleeper());
129
+
130
+ assertTrue(fatalExit.recordThenTerminate(() -> events.add("record")));
131
+
132
+ assertEquals(Collections.singletonList(TIMEOUT), sleeps);
133
+ assertEquals(Arrays.asList("record", "exit"), events);
134
+ }
135
+
136
+ @Test
137
+ public void interruptedFallbackWait_keepsWaitingUntilTheBound() {
138
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
139
+ fatalExit(
140
+ timeoutMs -> {
141
+ throw new InterruptedException("barrier interrupted");
142
+ },
143
+ millis -> {
144
+ sleeps.add(millis);
145
+ if (sleeps.size() == 1) {
146
+ now += 500;
147
+ throw new InterruptedException("sleep interrupted");
148
+ }
149
+ now += millis;
150
+ });
151
+
152
+ assertTrue(fatalExit.recordThenTerminate(() -> events.add("record")));
153
+
154
+ assertEquals(Arrays.asList(TIMEOUT, TIMEOUT - 500), sleeps);
155
+ assertEquals(Arrays.asList("record", "exit"), events);
156
+ }
157
+
158
+ @Test
159
+ public void secondFatalWhileTerminating_neitherRecordsNorTerminatesAgain() {
160
+ ReactNativeFirebaseCrashlyticsFatalExit[] holder =
161
+ new ReactNativeFirebaseCrashlyticsFatalExit[1];
162
+ List<Boolean> nestedResults = new ArrayList<>();
163
+ holder[0] =
164
+ fatalExit(
165
+ timeoutMs -> {
166
+ // A second JS fatal arrives while the first one is waiting on the barrier.
167
+ nestedResults.add(holder[0].recordThenTerminate(() -> events.add("record-2")));
168
+ },
169
+ advancingSleeper());
170
+
171
+ assertTrue(holder[0].recordThenTerminate(() -> events.add("record-1")));
172
+ assertFalse(holder[0].recordThenTerminate(() -> events.add("record-3")));
173
+
174
+ assertEquals(Collections.singletonList(false), nestedResults);
175
+ assertEquals(Arrays.asList("record-1", "exit"), events);
176
+ }
177
+
178
+ @Test
179
+ public void create_wiresTheProductionTimeoutClockAndSleeper() throws Exception {
180
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
181
+ ReactNativeFirebaseCrashlyticsFatalExit.create();
182
+
183
+ assertEquals(2000, ReactNativeFirebaseCrashlyticsFatalExit.TIMEOUT_MS);
184
+ assertEquals(ReactNativeFirebaseCrashlyticsFatalExit.TIMEOUT_MS, fatalExit.timeoutMs);
185
+ long before = fatalExit.clock.nowMillis();
186
+ fatalExit.sleeper.sleep(5);
187
+ assertTrue(fatalExit.clock.nowMillis() >= before + 5);
188
+ }
189
+ }
@@ -0,0 +1,199 @@
1
+ package io.invertase.firebase.crashlytics;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+ import static org.junit.Assert.assertEquals;
20
+ import static org.junit.Assert.assertFalse;
21
+ import static org.junit.Assert.assertNotNull;
22
+ import static org.junit.Assert.assertThrows;
23
+ import static org.junit.Assert.assertTrue;
24
+ import static org.mockito.Mockito.mock;
25
+
26
+ import com.google.android.gms.tasks.Task;
27
+ import com.google.android.gms.tasks.Tasks;
28
+ import com.google.firebase.crashlytics.FirebaseCrashlytics;
29
+ import com.google.firebase.crashlytics.internal.common.CrashlyticsCore;
30
+ import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorker;
31
+ import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers;
32
+ import java.lang.reflect.Constructor;
33
+ import java.lang.reflect.Field;
34
+ import java.util.concurrent.Callable;
35
+ import java.util.concurrent.CountDownLatch;
36
+ import java.util.concurrent.ExecutorService;
37
+ import java.util.concurrent.Executors;
38
+ import java.util.concurrent.TimeUnit;
39
+ import java.util.concurrent.atomic.AtomicBoolean;
40
+ import org.junit.After;
41
+ import org.junit.Test;
42
+ import org.junit.runner.RunWith;
43
+ import org.robolectric.RobolectricTestRunner;
44
+
45
+ /**
46
+ * JVM coverage for {@link ReactNativeFirebaseCrashlyticsInternal} against the real Crashlytics
47
+ * worker classes. Robolectric supplies the Android runtime the SDK classes load against;
48
+ * AndroidTest-AD-1. {@code Tasks.await} must not run on the (Robolectric) main thread, so waits
49
+ * happen on a pool thread.
50
+ */
51
+ @RunWith(RobolectricTestRunner.class)
52
+ public class ReactNativeFirebaseCrashlyticsInternalTest {
53
+ private final ExecutorService pool = Executors.newCachedThreadPool();
54
+
55
+ @After
56
+ public void tearDown() {
57
+ pool.shutdownNow();
58
+ }
59
+
60
+ /**
61
+ * A FirebaseCrashlytics whose core uses {@code workers}. Built by reflection because the real
62
+ * graph is impractical in a unit test: {@code CrashlyticsCore}'s only constructor takes ten SDK
63
+ * collaborators (FirebaseApp, file store, native component, ...) and {@code
64
+ * FirebaseCrashlytics}'s constructor is private. So a mock core gets its {@code private final}
65
+ * workers field set.
66
+ */
67
+ static FirebaseCrashlytics crashlyticsWith(CrashlyticsWorkers workers) throws Exception {
68
+ CrashlyticsCore core = mock(CrashlyticsCore.class);
69
+ Field field = CrashlyticsCore.class.getDeclaredField("crashlyticsWorkers");
70
+ field.setAccessible(true);
71
+ field.set(core, workers);
72
+ return crashlyticsWithCore(core);
73
+ }
74
+
75
+ static FirebaseCrashlytics crashlyticsWithCore(CrashlyticsCore core) throws Exception {
76
+ Constructor<FirebaseCrashlytics> constructor =
77
+ FirebaseCrashlytics.class.getDeclaredConstructor(CrashlyticsCore.class);
78
+ constructor.setAccessible(true);
79
+ return constructor.newInstance(core);
80
+ }
81
+
82
+ private <T> T awaitOnPool(Task<T> task) throws Exception {
83
+ return pool.submit(() -> Tasks.await(task, 5, TimeUnit.SECONDS)).get(10, TimeUnit.SECONDS);
84
+ }
85
+
86
+ @Test
87
+ public void barrier_completesAfterAnEventWriteQueuedByAnEarlierCommonTask() throws Exception {
88
+ CrashlyticsWorkers workers = new CrashlyticsWorkers(pool, pool);
89
+ CountDownLatch barrierCreated = new CountDownLatch(1);
90
+ AtomicBoolean eventWritten = new AtomicBoolean(false);
91
+
92
+ // Same shape as CrashlyticsCore.logException → SessionReportingCoordinator.persistEvent: a
93
+ // common task that queues the actual write on diskWrite.
94
+ workers.common.submit(
95
+ () -> {
96
+ try {
97
+ barrierCreated.await(5, TimeUnit.SECONDS);
98
+ } catch (InterruptedException e) {
99
+ throw new IllegalStateException(e);
100
+ }
101
+ workers.diskWrite.submit(
102
+ () -> {
103
+ try {
104
+ Thread.sleep(150);
105
+ } catch (InterruptedException e) {
106
+ throw new IllegalStateException(e);
107
+ }
108
+ eventWritten.set(true);
109
+ });
110
+ });
111
+
112
+ Task<?> barrier =
113
+ ReactNativeFirebaseCrashlyticsInternal.pendingEventWritesBarrier(crashlyticsWith(workers));
114
+ assertFalse(barrier.isComplete());
115
+ barrierCreated.countDown();
116
+
117
+ awaitOnPool(barrier);
118
+ assertTrue(eventWritten.get());
119
+ }
120
+
121
+ /** Every member the barrier reflects exists on the SDK version these tests compile against. */
122
+ @Test
123
+ public void reflectedMembers_existOnTheRealSdkClasses() throws Exception {
124
+ assertNotNull(
125
+ FirebaseCrashlytics.class.getDeclaredField(
126
+ ReactNativeFirebaseCrashlyticsInternal.CORE_FIELD));
127
+ assertNotNull(
128
+ CrashlyticsCore.class.getDeclaredField(
129
+ ReactNativeFirebaseCrashlyticsInternal.WORKERS_FIELD));
130
+ assertNotNull(
131
+ CrashlyticsWorkers.class.getDeclaredField(
132
+ ReactNativeFirebaseCrashlyticsInternal.COMMON_FIELD));
133
+ assertNotNull(
134
+ CrashlyticsWorkers.class.getDeclaredField(
135
+ ReactNativeFirebaseCrashlyticsInternal.DISK_WRITE_FIELD));
136
+ assertNotNull(
137
+ CrashlyticsWorker.class.getMethod(
138
+ ReactNativeFirebaseCrashlyticsInternal.SUBMIT_TASK_METHOD, Callable.class));
139
+ assertNotNull(
140
+ CrashlyticsWorker.class.getMethod(
141
+ ReactNativeFirebaseCrashlyticsInternal.SUBMIT_METHOD, Runnable.class));
142
+ }
143
+
144
+ @Test
145
+ public void barrier_nullCore_throws() throws Exception {
146
+ FirebaseCrashlytics crashlytics = crashlyticsWithCore(null);
147
+ NullPointerException thrown =
148
+ assertThrows(
149
+ NullPointerException.class,
150
+ () -> ReactNativeFirebaseCrashlyticsInternal.pendingEventWritesBarrier(crashlytics));
151
+ assertEquals("core", thrown.getMessage());
152
+ }
153
+
154
+ @Test
155
+ public void barrier_unsetWorkersField_throws() throws Exception {
156
+ FirebaseCrashlytics crashlytics = crashlyticsWithCore(mock(CrashlyticsCore.class));
157
+ NullPointerException thrown =
158
+ assertThrows(
159
+ NullPointerException.class,
160
+ () -> ReactNativeFirebaseCrashlyticsInternal.pendingEventWritesBarrier(crashlytics));
161
+ assertEquals("crashlyticsWorkers", thrown.getMessage());
162
+ }
163
+
164
+ @SuppressWarnings("unused")
165
+ private static class FieldOnBase {
166
+ private final Object workers = "from base";
167
+ }
168
+
169
+ private static final class SubclassWithoutTheField extends FieldOnBase {}
170
+
171
+ @Test
172
+ public void read_findsAFieldDeclaredOnASuperclass() throws Exception {
173
+ assertEquals(
174
+ "from base",
175
+ ReactNativeFirebaseCrashlyticsInternal.read(new SubclassWithoutTheField(), "workers"));
176
+ }
177
+
178
+ @Test
179
+ public void read_missingField_throwsNoSuchField() {
180
+ NoSuchFieldException thrown =
181
+ assertThrows(
182
+ NoSuchFieldException.class,
183
+ () -> ReactNativeFirebaseCrashlyticsInternal.read(new Object(), "crashlyticsWorkers"));
184
+ assertEquals("crashlyticsWorkers", thrown.getMessage());
185
+ }
186
+
187
+ @SuppressWarnings("unused")
188
+ private static final class WorkersWithoutSubmitMethods {
189
+ private final Object common = new Object();
190
+ private final Object diskWrite = new Object();
191
+ }
192
+
193
+ @Test
194
+ public void barrierOn_missingWorkerMethod_throwsNoSuchMethod() {
195
+ assertThrows(
196
+ NoSuchMethodException.class,
197
+ () -> ReactNativeFirebaseCrashlyticsInternal.barrierOn(new WorkersWithoutSubmitMethods()));
198
+ }
199
+ }
@@ -0,0 +1,108 @@
1
+ package io.invertase.firebase.crashlytics;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import static org.junit.Assert.assertSame;
21
+ import static org.junit.Assert.assertTrue;
22
+ import static org.mockito.Mockito.mock;
23
+ import static org.mockito.Mockito.mockStatic;
24
+ import static org.mockito.Mockito.verify;
25
+
26
+ import com.google.firebase.crashlytics.FirebaseCrashlytics;
27
+ import java.lang.reflect.Method;
28
+ import java.lang.reflect.Modifier;
29
+ import java.util.Arrays;
30
+ import org.junit.Test;
31
+ import org.junit.runner.RunWith;
32
+ import org.mockito.MockedStatic;
33
+ import org.robolectric.RobolectricTestRunner;
34
+
35
+ /**
36
+ * JVM coverage for {@link ReactNativeFirebaseCrashlyticsNativeHelper}. Stubs {@link
37
+ * FirebaseCrashlytics#getInstance()} via mockito-mockstatic so the helper can be exercised without
38
+ * initializing the Firebase SDK. Robolectric supplies an Android runtime for Firebase class
39
+ * loading; AndroidTest-AD-1.
40
+ */
41
+ @RunWith(RobolectricTestRunner.class)
42
+ public class ReactNativeFirebaseCrashlyticsNativeHelperTest {
43
+
44
+ @Test
45
+ public void javaStaticShapePreservedForNativeCallers() throws Exception {
46
+ assertTrue(Modifier.isPublic(ReactNativeFirebaseCrashlyticsNativeHelper.class.getModifiers()));
47
+ assertTrue(Modifier.isFinal(ReactNativeFirebaseCrashlyticsNativeHelper.class.getModifiers()));
48
+ assertSame(
49
+ ReactNativeFirebaseCrashlyticsNativeHelper.INSTANCE,
50
+ ReactNativeFirebaseCrashlyticsNativeHelper.INSTANCE);
51
+
52
+ Method record =
53
+ ReactNativeFirebaseCrashlyticsNativeHelper.class.getMethod(
54
+ "recordNativeException", Throwable.class);
55
+ Method log = ReactNativeFirebaseCrashlyticsNativeHelper.class.getMethod("log", String.class);
56
+ Method setCustomKey =
57
+ ReactNativeFirebaseCrashlyticsNativeHelper.class.getMethod(
58
+ "setCustomKey", String.class, String.class);
59
+
60
+ for (Method method : Arrays.asList(record, log, setCustomKey)) {
61
+ assertTrue(method.getName(), Modifier.isStatic(method.getModifiers()));
62
+ assertTrue(method.getName(), Modifier.isPublic(method.getModifiers()));
63
+ }
64
+ }
65
+
66
+ @Test
67
+ public void recordNativeException_forwardsToFirebaseCrashlytics() {
68
+ FirebaseCrashlytics crashlytics = mock(FirebaseCrashlytics.class);
69
+ Throwable throwable = new RuntimeException("native-boom");
70
+
71
+ try (MockedStatic<FirebaseCrashlytics> firebaseCrashlytics =
72
+ mockStatic(FirebaseCrashlytics.class)) {
73
+ firebaseCrashlytics.when(FirebaseCrashlytics::getInstance).thenReturn(crashlytics);
74
+
75
+ ReactNativeFirebaseCrashlyticsNativeHelper.recordNativeException(throwable);
76
+
77
+ verify(crashlytics).recordException(throwable);
78
+ }
79
+ }
80
+
81
+ @Test
82
+ public void log_forwardsToFirebaseCrashlytics() {
83
+ FirebaseCrashlytics crashlytics = mock(FirebaseCrashlytics.class);
84
+
85
+ try (MockedStatic<FirebaseCrashlytics> firebaseCrashlytics =
86
+ mockStatic(FirebaseCrashlytics.class)) {
87
+ firebaseCrashlytics.when(FirebaseCrashlytics::getInstance).thenReturn(crashlytics);
88
+
89
+ ReactNativeFirebaseCrashlyticsNativeHelper.log("native-log");
90
+
91
+ verify(crashlytics).log("native-log");
92
+ }
93
+ }
94
+
95
+ @Test
96
+ public void setCustomKey_forwardsToFirebaseCrashlytics() {
97
+ FirebaseCrashlytics crashlytics = mock(FirebaseCrashlytics.class);
98
+
99
+ try (MockedStatic<FirebaseCrashlytics> firebaseCrashlytics =
100
+ mockStatic(FirebaseCrashlytics.class)) {
101
+ firebaseCrashlytics.when(FirebaseCrashlytics::getInstance).thenReturn(crashlytics);
102
+
103
+ ReactNativeFirebaseCrashlyticsNativeHelper.setCustomKey("k", "v");
104
+
105
+ verify(crashlytics).setCustomKey("k", "v");
106
+ }
107
+ }
108
+ }
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  // Generated by genversion.
4
- export const version = '26.4.0';
4
+ export const version = '27.1.0';
5
5
  //# sourceMappingURL=version.js.map
@@ -1,7 +1,7 @@
1
1
  import type { FirebaseApp } from '@react-native-firebase/app';
2
2
  import './types/internal';
3
3
  import type { Crashlytics } from './types/crashlytics';
4
- export declare const SDK_VERSION = "26.4.0";
4
+ export declare const SDK_VERSION = "27.1.0";
5
5
  export declare function getCrashlytics(app?: FirebaseApp): Crashlytics;
6
6
  export declare function checkForUnsentReports(crashlytics: Crashlytics): Promise<boolean>;
7
7
  export declare function deleteUnsentReports(crashlytics: Crashlytics): Promise<void>;
@@ -1,2 +1,2 @@
1
- export declare const version = "26.4.0";
1
+ export declare const version = "27.1.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import Darwin
18
+ import Foundation
19
+
20
+ /**
21
+ * Debugger-attach detection previously inline in `RNFBCrashlyticsModule.mm`
22
+ * (`-isDebuggerAttached`).
23
+ *
24
+ * Pre-port contracts:
25
+ * - Once-token / dispatch_once-style caching of the result
26
+ * - `sysctl` `CTL_KERN` / `KERN_PROC` / `KERN_PROC_PID` / `getpid()` → `kinfo_proc`
27
+ * - `sysctl` failure → ELog-equivalent + treat as not attached (then still evaluate
28
+ * `P_TRACED` on the returned buffer, matching the pre-port control flow)
29
+ * - `P_TRACED` bit set → true
30
+ *
31
+ * Foundation + Darwin only so unit tests do not link Firebase / React.
32
+ */
33
+ @objc(RNFBCrashlyticsDebuggerProbe)
34
+ public final class RNFBCrashlyticsDebuggerProbe: NSObject {
35
+
36
+ /// Injectable `sysctl` seam for unit tests. Production uses `makeDefaultSysctl()`.
37
+ typealias SysctlProbe = (
38
+ _ name: UnsafeMutablePointer<Int32>,
39
+ _ namelen: u_int,
40
+ _ oldp: UnsafeMutableRawPointer?,
41
+ _ oldlenp: UnsafeMutablePointer<Int>,
42
+ _ newp: UnsafeMutableRawPointer?,
43
+ _ newlen: Int
44
+ ) -> Int32
45
+
46
+ private static let lock = NSLock()
47
+ private static var cachedResult: Bool?
48
+ /// Mirrors `static dispatch_once_t` — evaluate at most once in production path.
49
+ private static var onceTokenConsumed = false
50
+
51
+ /// Test hook: replace `sysctl`. Restored by `resetForTesting()`.
52
+ static var sysctlImpl: SysctlProbe = makeDefaultSysctl()
53
+
54
+ private static func makeDefaultSysctl() -> SysctlProbe {
55
+ { name, namelen, oldp, oldlenp, newp, newlen in
56
+ Darwin.sysctl(name, namelen, oldp, oldlenp, newp, newlen)
57
+ }
58
+ }
59
+
60
+ /// Pre-port `-isDebuggerAttached` (once-cached).
61
+ @objc
62
+ public static func isDebuggerAttached() -> Bool {
63
+ lock.lock()
64
+ defer { lock.unlock() }
65
+ if onceTokenConsumed, let cached = cachedResult {
66
+ return cached
67
+ }
68
+ let value = evaluateIsDebuggerAttached(using: sysctlImpl)
69
+ cachedResult = value
70
+ onceTokenConsumed = true
71
+ return value
72
+ }
73
+
74
+ /// Clears once-cache and restores the default `sysctl` implementation (tests only).
75
+ static func resetForTesting() {
76
+ lock.lock()
77
+ defer { lock.unlock() }
78
+ cachedResult = nil
79
+ onceTokenConsumed = false
80
+ sysctlImpl = makeDefaultSysctl()
81
+ }
82
+
83
+ /// Evaluates without consuming the production once-cache (tests only).
84
+ static func evaluateUncachedForTesting(using probe: SysctlProbe) -> Bool {
85
+ evaluateIsDebuggerAttached(using: probe)
86
+ }
87
+
88
+ /// Core logic matching the pre-port `dispatch_once` body.
89
+ private static func evaluateIsDebuggerAttached(using probe: SysctlProbe) -> Bool {
90
+ var debuggerIsAttached = false
91
+
92
+ var info = kinfo_proc()
93
+ var infoSize = MemoryLayout<kinfo_proc>.size
94
+ var name: [Int32] = [
95
+ CTL_KERN,
96
+ KERN_PROC,
97
+ KERN_PROC_PID,
98
+ getpid(),
99
+ ]
100
+
101
+ // `name` is always 4 elements — `baseAddress` is non-nil.
102
+ let sysctlResult = name.withUnsafeMutableBufferPointer { nameBuf -> Int32 in
103
+ probe(nameBuf.baseAddress!, 4, &info, &infoSize, nil, 0)
104
+ }
105
+
106
+ if sysctlResult == -1 {
107
+ // ELog is NSLog with function/line prefix (RNFBSharedUtils.h).
108
+ let err = String(cString: strerror(errno))
109
+ NSLog(
110
+ "%@ [Line %d] Crashlytics ERROR: Checking for a running debugger via sysctl() failed: %@",
111
+ #function, #line, err)
112
+ debuggerIsAttached = false
113
+ }
114
+
115
+ if !debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0 {
116
+ debuggerIsAttached = true
117
+ }
118
+
119
+ return debuggerIsAttached
120
+ }
121
+ }