@react-native-firebase/crashlytics 27.0.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 +8 -0
- package/android/build.gradle +1 -0
- package/android/proguard-rules.pro +20 -0
- 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/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/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/RNFBCrashlyticsHelper.h +40 -0
- package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.m +73 -0
- package/ios/RNFBCrashlytics/RNFBCrashlyticsModule.mm +21 -7
- package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/FIRCrashlyticsHostStub.m +66 -0
- package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/Firebase.h +9 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsFatalExitTests.m +268 -0
- package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/project.pbxproj +4 -0
- package/ios_config.sh +14 -4
- package/lib/version.ts +1 -1
- package/package.json +4 -4
|
@@ -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
|
+
}
|
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 = "27.
|
|
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 = "27.
|
|
1
|
+
export declare const version = "27.1.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
|
@@ -56,3 +56,43 @@
|
|
|
56
56
|
isUnhandledPromiseRejection:(BOOL)isUnhandledPromiseRejection;
|
|
57
57
|
|
|
58
58
|
@end
|
|
59
|
+
|
|
60
|
+
/** Upper bound (seconds) a JS fatal waits for its record to save before the process exits. */
|
|
61
|
+
FOUNDATION_EXPORT const NSTimeInterval RNFBCrashlyticsFatalExitWatchdogTimeout;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Records a JS fatal, then terminates the process once the record has had a chance to save.
|
|
65
|
+
*
|
|
66
|
+
* `-[FIRCrashlytics recordExceptionModel:]` saves asynchronously: once Crashlytics' context-init
|
|
67
|
+
* promise is fulfilled, the save block is dispatched to the main queue. Terminating on the
|
|
68
|
+
* calling queue right after recording usually loses that save. Instead the terminator is
|
|
69
|
+
* enqueued on `terminateQueue` (the main queue in production), so it runs FIFO after the save
|
|
70
|
+
* block. A watchdog on `watchdogQueue` terminates after `watchdogTimeout` if `terminateQueue`
|
|
71
|
+
* is hung. The terminator runs at most once.
|
|
72
|
+
*
|
|
73
|
+
* Known limit: a JS fatal recorded before Crashlytics finished initialising queues its save as a
|
|
74
|
+
* promise observer instead of a main-queue block, so it can still be lost.
|
|
75
|
+
* The FIFO ordering also relies on FBLPromise's default dispatch queue being the main queue; if an
|
|
76
|
+
* app changes it, the save runs off the main queue and the terminator can run first, losing the
|
|
77
|
+
* report. The watchdog only covers a hung `terminateQueue`.
|
|
78
|
+
*/
|
|
79
|
+
@interface RNFBCrashlyticsFatalExit : NSObject
|
|
80
|
+
|
|
81
|
+
/** Production instance: terminates with `exit(0)` on the main queue, 2 s watchdog. */
|
|
82
|
+
+ (instancetype)shared;
|
|
83
|
+
|
|
84
|
+
- (instancetype)initWithTerminator:(dispatch_block_t)terminator
|
|
85
|
+
terminateQueue:(dispatch_queue_t)terminateQueue
|
|
86
|
+
watchdogQueue:(dispatch_queue_t)watchdogQueue
|
|
87
|
+
watchdogTimeout:(NSTimeInterval)watchdogTimeout NS_DESIGNATED_INITIALIZER;
|
|
88
|
+
|
|
89
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Runs `record` synchronously, then schedules termination, even when `record` raises. Returns NO
|
|
93
|
+
* without running `record` when an earlier call already started terminating (a second fatal
|
|
94
|
+
* while waiting).
|
|
95
|
+
*/
|
|
96
|
+
- (BOOL)recordThenTerminate:(dispatch_block_t)record;
|
|
97
|
+
|
|
98
|
+
@end
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
#error "RNFBCrashlyticsDebuggerProbe Swift interface not found"
|
|
36
36
|
#endif
|
|
37
37
|
|
|
38
|
+
#import <stdatomic.h>
|
|
39
|
+
|
|
38
40
|
#import "RNFBCrashlyticsHelper.h"
|
|
39
41
|
|
|
40
42
|
@implementation RNFBCrashlyticsHelper
|
|
@@ -107,3 +109,74 @@
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
@end
|
|
112
|
+
|
|
113
|
+
const NSTimeInterval RNFBCrashlyticsFatalExitWatchdogTimeout = 2.0;
|
|
114
|
+
|
|
115
|
+
@implementation RNFBCrashlyticsFatalExit {
|
|
116
|
+
dispatch_block_t _terminator;
|
|
117
|
+
dispatch_queue_t _terminateQueue;
|
|
118
|
+
dispatch_queue_t _watchdogQueue;
|
|
119
|
+
NSTimeInterval _watchdogTimeout;
|
|
120
|
+
atomic_bool _exiting;
|
|
121
|
+
atomic_bool _terminated;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
+ (instancetype)shared {
|
|
125
|
+
static RNFBCrashlyticsFatalExit *shared;
|
|
126
|
+
static dispatch_once_t once;
|
|
127
|
+
dispatch_once(&once, ^{
|
|
128
|
+
shared = [[RNFBCrashlyticsFatalExit alloc]
|
|
129
|
+
initWithTerminator:^{
|
|
130
|
+
// Coverage exception: ends the process, so no test can run it.
|
|
131
|
+
exit(0);
|
|
132
|
+
}
|
|
133
|
+
terminateQueue:dispatch_get_main_queue()
|
|
134
|
+
watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
|
|
135
|
+
watchdogTimeout:RNFBCrashlyticsFatalExitWatchdogTimeout];
|
|
136
|
+
});
|
|
137
|
+
return shared;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
- (instancetype)initWithTerminator:(dispatch_block_t)terminator
|
|
141
|
+
terminateQueue:(dispatch_queue_t)terminateQueue
|
|
142
|
+
watchdogQueue:(dispatch_queue_t)watchdogQueue
|
|
143
|
+
watchdogTimeout:(NSTimeInterval)watchdogTimeout {
|
|
144
|
+
if (self = [super init]) {
|
|
145
|
+
_terminator = [terminator copy];
|
|
146
|
+
_terminateQueue = terminateQueue;
|
|
147
|
+
_watchdogQueue = watchdogQueue;
|
|
148
|
+
_watchdogTimeout = watchdogTimeout;
|
|
149
|
+
atomic_init(&_exiting, false);
|
|
150
|
+
atomic_init(&_terminated, false);
|
|
151
|
+
}
|
|
152
|
+
return self;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
- (BOOL)recordThenTerminate:(dispatch_block_t)record {
|
|
156
|
+
if (atomic_exchange(&_exiting, true)) {
|
|
157
|
+
return NO;
|
|
158
|
+
}
|
|
159
|
+
@try {
|
|
160
|
+
record();
|
|
161
|
+
} @catch (NSException *exception) {
|
|
162
|
+
// The guard is already set, so terminate anyway rather than leave the app running with every
|
|
163
|
+
// later fatal ignored.
|
|
164
|
+
NSLog(@"RNFBCrashlytics: recording JS fatal failed: %@", exception);
|
|
165
|
+
}
|
|
166
|
+
dispatch_block_t terminateOnce = ^{
|
|
167
|
+
[self terminateOnce];
|
|
168
|
+
};
|
|
169
|
+
// FIFO behind the Crashlytics save block that recordExceptionModel: enqueued on main.
|
|
170
|
+
dispatch_async(_terminateQueue, terminateOnce);
|
|
171
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_watchdogTimeout * NSEC_PER_SEC)),
|
|
172
|
+
_watchdogQueue, terminateOnce);
|
|
173
|
+
return YES;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
- (void)terminateOnce {
|
|
177
|
+
if (!atomic_exchange(&_terminated, true)) {
|
|
178
|
+
_terminator();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@end
|
|
@@ -97,10 +97,15 @@ RCT_EXPORT_MODULE(NativeRNFBTurboCrashlytics)
|
|
|
97
97
|
RCTLog(
|
|
98
98
|
@"Crashlytics - WARNING: Debugger detected. Crashlytics will not receive crash reports.");
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
// Coverage exception: this branch ends the process, so neither XCTest nor e2e can observe it.
|
|
101
|
+
// Keep it a thin delegate; ordering, watchdog and the second-fatal guard live in
|
|
102
|
+
// RNFBCrashlyticsFatalExit (unit-tested). The promise is intentionally never resolved: that
|
|
103
|
+
// keeps the JS global handler suspended (no chaining to the previous handler, no
|
|
104
|
+
// fall-through) until termination.
|
|
105
|
+
ELog(@"Crashlytics - Logging crash and terminating app.");
|
|
106
|
+
[[RNFBCrashlyticsFatalExit shared]
|
|
107
|
+
recordThenTerminate:[self javaScriptErrorRecorder:jsErrorDict]];
|
|
108
|
+
return;
|
|
104
109
|
} else {
|
|
105
110
|
RCTLog(@"Crashlytics - INFO: crashlytics collection is not enabled, not crashing.");
|
|
106
111
|
}
|
|
@@ -193,6 +198,13 @@ RCT_EXPORT_MODULE(NativeRNFBTurboCrashlytics)
|
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
- (void)recordJavaScriptError:(JS::NativeRNFBTurboCrashlytics::JavaScriptErrorObject &)jsErrorDict {
|
|
201
|
+
[self javaScriptErrorRecorder:jsErrorDict]();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Converts the codegen struct to Foundation values up front, so the returned block captures no
|
|
205
|
+
// C++ references and can be handed to RNFBCrashlyticsFatalExit.
|
|
206
|
+
- (dispatch_block_t)javaScriptErrorRecorder:
|
|
207
|
+
(JS::NativeRNFBTurboCrashlytics::JavaScriptErrorObject &)jsErrorDict {
|
|
196
208
|
NSString *message = jsErrorDict.message();
|
|
197
209
|
auto stackFrames = jsErrorDict.frames();
|
|
198
210
|
NSMutableArray *frames = [[NSMutableArray alloc] init];
|
|
@@ -206,9 +218,11 @@ RCT_EXPORT_MODULE(NativeRNFBTurboCrashlytics)
|
|
|
206
218
|
}];
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
return ^{
|
|
222
|
+
[RNFBCrashlyticsHelper recordJavaScriptErrorWithMessage:message
|
|
223
|
+
frames:frames
|
|
224
|
+
isUnhandledPromiseRejection:isUnhandledPromiseRejection];
|
|
225
|
+
};
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
@end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Host-only stub for macOS XCTest. Not shipped in the production pod.
|
|
3
3
|
*/
|
|
4
4
|
#import <Firebase/Firebase.h>
|
|
5
|
+
#import <stdatomic.h>
|
|
5
6
|
|
|
6
7
|
@implementation FIRStackFrame {
|
|
7
8
|
NSString *_symbol;
|
|
@@ -63,11 +64,19 @@ static NSString *sUserIDValue;
|
|
|
63
64
|
static NSUInteger sDeleteUnsentReportsCallCount;
|
|
64
65
|
static NSUInteger sSendUnsentReportsCallCount;
|
|
65
66
|
static FIRExceptionModel *sLastRecordedExceptionModel;
|
|
67
|
+
static NSMutableArray<FIRExceptionModel *> *sSavedExceptionModels;
|
|
68
|
+
static void (^sOnExceptionModelSaved)(FIRExceptionModel *);
|
|
69
|
+
static BOOL sContextInitPending;
|
|
70
|
+
static NSMutableArray<dispatch_block_t> *sPendingSaves;
|
|
71
|
+
// Bumped by resetTestState so saves enqueued by an earlier test are dropped.
|
|
72
|
+
static atomic_uint_fast64_t sSaveGeneration;
|
|
66
73
|
|
|
67
74
|
+ (void)initialize {
|
|
68
75
|
if (self == [FIRCrashlytics class]) {
|
|
69
76
|
sLoggedMessages = [NSMutableArray new];
|
|
70
77
|
sCustomValues = [NSMutableDictionary new];
|
|
78
|
+
sSavedExceptionModels = [NSMutableArray new];
|
|
79
|
+
sPendingSaves = [NSMutableArray new];
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -150,6 +159,36 @@ static FIRExceptionModel *sLastRecordedExceptionModel;
|
|
|
150
159
|
sLastRecordedExceptionModel = lastRecordedExceptionModel;
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
+ (NSArray<FIRExceptionModel *> *)savedExceptionModels {
|
|
163
|
+
return [sSavedExceptionModels copy];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
+ (void (^)(FIRExceptionModel *))onExceptionModelSaved {
|
|
167
|
+
return sOnExceptionModelSaved;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
+ (void)setOnExceptionModelSaved:(void (^)(FIRExceptionModel *))onExceptionModelSaved {
|
|
171
|
+
sOnExceptionModelSaved = [onExceptionModelSaved copy];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
+ (void)holdContextInit {
|
|
175
|
+
@synchronized(sPendingSaves) {
|
|
176
|
+
sContextInitPending = YES;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
+ (void)fulfillContextInit {
|
|
181
|
+
NSArray<dispatch_block_t> *saves;
|
|
182
|
+
@synchronized(sPendingSaves) {
|
|
183
|
+
sContextInitPending = NO;
|
|
184
|
+
saves = [sPendingSaves copy];
|
|
185
|
+
[sPendingSaves removeAllObjects];
|
|
186
|
+
}
|
|
187
|
+
for (dispatch_block_t save in saves) {
|
|
188
|
+
dispatch_async(dispatch_get_main_queue(), save);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
153
192
|
+ (void)resetTestState {
|
|
154
193
|
sDidCrashDuringPreviousExecutionValue = NO;
|
|
155
194
|
sCheckForUnsentReportsValue = NO;
|
|
@@ -159,6 +198,13 @@ static FIRExceptionModel *sLastRecordedExceptionModel;
|
|
|
159
198
|
sDeleteUnsentReportsCallCount = 0;
|
|
160
199
|
sSendUnsentReportsCallCount = 0;
|
|
161
200
|
sLastRecordedExceptionModel = nil;
|
|
201
|
+
[sSavedExceptionModels removeAllObjects];
|
|
202
|
+
sOnExceptionModelSaved = nil;
|
|
203
|
+
@synchronized(sPendingSaves) {
|
|
204
|
+
sContextInitPending = NO;
|
|
205
|
+
[sPendingSaves removeAllObjects];
|
|
206
|
+
}
|
|
207
|
+
atomic_fetch_add(&sSaveGeneration, 1);
|
|
162
208
|
}
|
|
163
209
|
|
|
164
210
|
- (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL unsentReports))completion {
|
|
@@ -202,6 +248,26 @@ static FIRExceptionModel *sLastRecordedExceptionModel;
|
|
|
202
248
|
|
|
203
249
|
- (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel {
|
|
204
250
|
sLastRecordedExceptionModel = exceptionModel;
|
|
251
|
+
// Mirrors FirebaseCrashlytics: once context init is fulfilled, the save block is dispatched to
|
|
252
|
+
// the main queue and writes synchronously when it runs. While init is pending, FBLPromise holds
|
|
253
|
+
// the block as an observer and dispatches it to the main queue only when the promise fulfills.
|
|
254
|
+
uint_fast64_t generation = atomic_load(&sSaveGeneration);
|
|
255
|
+
dispatch_block_t save = ^{
|
|
256
|
+
if (generation != atomic_load(&sSaveGeneration)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
[sSavedExceptionModels addObject:exceptionModel];
|
|
260
|
+
if (sOnExceptionModelSaved != nil) {
|
|
261
|
+
sOnExceptionModelSaved(exceptionModel);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
@synchronized(sPendingSaves) {
|
|
265
|
+
if (sContextInitPending) {
|
|
266
|
+
[sPendingSaves addObject:save];
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
dispatch_async(dispatch_get_main_queue(), save);
|
|
205
271
|
}
|
|
206
272
|
|
|
207
273
|
@end
|
|
@@ -41,6 +41,15 @@
|
|
|
41
41
|
@property(class, nonatomic, assign) NSUInteger deleteUnsentReportsCallCount;
|
|
42
42
|
@property(class, nonatomic, assign) NSUInteger sendUnsentReportsCallCount;
|
|
43
43
|
@property(class, nonatomic, copy, nullable) FIRExceptionModel *lastRecordedExceptionModel;
|
|
44
|
+
/** Models whose main-queue save block has run (see recordExceptionModel:). */
|
|
45
|
+
@property(class, nonatomic, readonly, copy) NSArray<FIRExceptionModel *> *savedExceptionModels;
|
|
46
|
+
/** Called on the main queue right after a save block runs. */
|
|
47
|
+
@property(class, nonatomic, copy, nullable) void (^onExceptionModelSaved)(FIRExceptionModel *model);
|
|
48
|
+
/** Holds later saves like observers of a pending context-init promise, until fulfillContextInit. */
|
|
49
|
+
+ (void)holdContextInit;
|
|
50
|
+
|
|
51
|
+
/** Dispatches the held saves to the main queue and stops holding new ones. */
|
|
52
|
+
+ (void)fulfillContextInit;
|
|
44
53
|
|
|
45
54
|
+ (void)resetTestState;
|
|
46
55
|
|