@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.
- package/CHANGELOG.md +23 -0
- package/RNFBCrashlytics.podspec +7 -1
- package/android/build.gradle +8 -0
- package/android/proguard-rules.pro +20 -0
- package/android/src/main/java/io/invertase/firebase/crashlytics/Constants.kt +42 -0
- package/android/src/main/java/io/invertase/firebase/crashlytics/{JavaScriptError.java → JavaScriptError.kt} +4 -6
- package/android/src/main/java/io/invertase/firebase/crashlytics/NativeRNFBTurboCrashlytics.java +11 -4
- package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExit.java +117 -0
- package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternal.java +80 -0
- package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelper.kt +45 -0
- package/android/src/main/java/io/invertase/firebase/crashlytics/{UnhandledPromiseRejection.java → UnhandledPromiseRejection.kt} +4 -6
- package/android/src/test/java/io/invertase/firebase/crashlytics/ConstantsAndJsErrorTypesTest.java +84 -0
- package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitBarrierTest.java +122 -0
- package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitTest.java +189 -0
- package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternalTest.java +199 -0
- package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelperTest.java +108 -0
- package/dist/module/version.js +1 -1
- package/dist/typescript/lib/index.d.ts +1 -1
- package/dist/typescript/lib/version.d.ts +1 -1
- package/ios/RNFBCrashlytics/RNFBCrashlyticsDebuggerProbe.swift +121 -0
- package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.h +98 -0
- package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.m +182 -0
- package/ios/RNFBCrashlytics/RNFBCrashlyticsModule.mm +57 -75
- package/ios/RNFBCrashlytics.xcodeproj/project.pbxproj +14 -0
- package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/FIRCrashlyticsHostStub.m +273 -0
- package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/Firebase.h +56 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsDebuggerProbeTests.swift +112 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsFatalExitTests.m +268 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsHelperHostStubTests.m +119 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/project.pbxproj +265 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBCrashlyticsUnitTests.xcscheme +69 -0
- package/ios_config.sh +38 -5
- package/lib/version.ts +1 -1
- package/package.json +4 -4
- package/android/src/main/java/io/invertase/firebase/crashlytics/Constants.java +0 -29
- 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
|
+
}
|
package/dist/module/version.js
CHANGED
|
@@ -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 = "
|
|
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 = "
|
|
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
|
+
}
|