@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.
Files changed (22) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/android/build.gradle +1 -0
  3. package/android/proguard-rules.pro +20 -0
  4. package/android/src/main/java/io/invertase/firebase/crashlytics/NativeRNFBTurboCrashlytics.java +11 -4
  5. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExit.java +117 -0
  6. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternal.java +80 -0
  7. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitBarrierTest.java +122 -0
  8. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitTest.java +189 -0
  9. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternalTest.java +199 -0
  10. package/dist/module/version.js +1 -1
  11. package/dist/typescript/lib/index.d.ts +1 -1
  12. package/dist/typescript/lib/version.d.ts +1 -1
  13. package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.h +40 -0
  14. package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.m +73 -0
  15. package/ios/RNFBCrashlytics/RNFBCrashlyticsModule.mm +21 -7
  16. package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/FIRCrashlyticsHostStub.m +66 -0
  17. package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/Firebase.h +9 -0
  18. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsFatalExitTests.m +268 -0
  19. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/project.pbxproj +4 -0
  20. package/ios_config.sh +14 -4
  21. package/lib/version.ts +1 -1
  22. package/package.json +4 -4
@@ -0,0 +1,268 @@
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 <XCTest/XCTest.h>
18
+
19
+ #import <Firebase/Firebase.h>
20
+ #import "RNFBCrashlyticsHelper.h"
21
+
22
+ @interface RNFBCrashlyticsFatalExitTests : XCTestCase
23
+ @end
24
+
25
+ @implementation RNFBCrashlyticsFatalExitTests {
26
+ NSMutableArray<NSString *> *_events;
27
+ }
28
+
29
+ - (void)setUp {
30
+ [super setUp];
31
+ [FIRCrashlytics resetTestState];
32
+ _events = [NSMutableArray new];
33
+ }
34
+
35
+ - (void)tearDown {
36
+ [FIRCrashlytics resetTestState];
37
+ [super tearDown];
38
+ }
39
+
40
+ - (void)appendEvent:(NSString *)event {
41
+ @synchronized(_events) {
42
+ [_events addObject:event];
43
+ }
44
+ }
45
+
46
+ - (NSArray<NSString *> *)eventsSnapshot {
47
+ @synchronized(_events) {
48
+ return [_events copy];
49
+ }
50
+ }
51
+
52
+ - (void)recordFatal {
53
+ [RNFBCrashlyticsHelper recordJavaScriptErrorWithMessage:@"fatal"
54
+ frames:@[]
55
+ isUnhandledPromiseRejection:NO];
56
+ }
57
+
58
+ /** Lets late watchdog / queue blocks run so "at most once" assertions are meaningful. */
59
+ - (void)drainFor:(NSTimeInterval)seconds {
60
+ XCTestExpectation *drained = [self expectationWithDescription:@"drained"];
61
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)),
62
+ dispatch_get_main_queue(), ^{
63
+ [drained fulfill];
64
+ });
65
+ [self waitForExpectations:@[ drained ] timeout:seconds + 5];
66
+ }
67
+
68
+ /** Returns once every block already enqueued on serial `queue` has run (a happens-before edge). */
69
+ - (void)drainQueue:(dispatch_queue_t)queue {
70
+ dispatch_block_t noop = ^{
71
+ };
72
+ dispatch_sync(queue, noop);
73
+ }
74
+
75
+ - (void)testShared_isProductionSingleton {
76
+ XCTAssertNotNil([RNFBCrashlyticsFatalExit shared]);
77
+ XCTAssertEqual([RNFBCrashlyticsFatalExit shared], [RNFBCrashlyticsFatalExit shared]);
78
+ XCTAssertEqualWithAccuracy(RNFBCrashlyticsFatalExitWatchdogTimeout, 2.0, 0.0001);
79
+ }
80
+
81
+ - (void)testRecordThenTerminate_savesOnMainBeforeTerminatorRuns {
82
+ FIRCrashlytics.onExceptionModelSaved = ^(FIRExceptionModel *model) {
83
+ [self appendEvent:[@"save:" stringByAppendingString:model.reason]];
84
+ };
85
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
86
+ __block NSUInteger terminations = 0;
87
+ // Held suspended until the main-queue terminate has run, so the watchdog cannot win under load.
88
+ dispatch_queue_t watchdogQueue =
89
+ dispatch_queue_create("rnfb.crashlytics.test.watchdog", DISPATCH_QUEUE_SERIAL);
90
+ dispatch_suspend(watchdogQueue);
91
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
92
+ initWithTerminator:^{
93
+ terminations += 1;
94
+ [self appendEvent:@"exit"];
95
+ XCTAssertTrue([NSThread isMainThread]);
96
+ [terminated fulfill];
97
+ }
98
+ terminateQueue:dispatch_get_main_queue()
99
+ watchdogQueue:watchdogQueue
100
+ watchdogTimeout:0.2];
101
+
102
+ // Production calls in on the module's own (non-main) method queue.
103
+ __block BOOL started = NO;
104
+ dispatch_queue_t moduleQueue =
105
+ dispatch_queue_create("rnfb.crashlytics.test.module", DISPATCH_QUEUE_SERIAL);
106
+ dispatch_async(moduleQueue, ^{
107
+ started = [fatalExit recordThenTerminate:^{
108
+ [self recordFatal];
109
+ }];
110
+ });
111
+
112
+ [self waitForExpectations:@[ terminated ] timeout:5];
113
+ dispatch_resume(watchdogQueue);
114
+ [self drainFor:0.4]; // past the watchdog, which must not terminate again
115
+ [self drainQueue:watchdogQueue];
116
+ [self drainQueue:moduleQueue]; // `started` is written on moduleQueue
117
+
118
+ XCTAssertTrue(started);
119
+ XCTAssertEqual(terminations, 1u);
120
+ NSArray *expected = @[ @"save:fatal", @"exit" ];
121
+ XCTAssertEqualObjects([self eventsSnapshot], expected);
122
+ XCTAssertEqual(FIRCrashlytics.savedExceptionModels.count, 1u);
123
+ XCTAssertEqualObjects(FIRCrashlytics.savedExceptionModels.firstObject.name, @"JavaScriptError");
124
+ }
125
+
126
+ - (void)testRecordThenTerminate_contextInitPending_terminatesBeforeSave {
127
+ [FIRCrashlytics holdContextInit];
128
+ FIRCrashlytics.onExceptionModelSaved = ^(FIRExceptionModel *model) {
129
+ [self appendEvent:[@"save:" stringByAppendingString:model.reason]];
130
+ };
131
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
132
+ __block NSUInteger terminations = 0;
133
+ __block NSUInteger savedAtExit = NSUIntegerMax;
134
+ __block BOOL recordedAtExit = NO;
135
+ dispatch_queue_t watchdogQueue =
136
+ dispatch_queue_create("rnfb.crashlytics.test.watchdog", DISPATCH_QUEUE_SERIAL);
137
+ dispatch_suspend(watchdogQueue);
138
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
139
+ initWithTerminator:^{
140
+ terminations += 1;
141
+ savedAtExit = FIRCrashlytics.savedExceptionModels.count;
142
+ recordedAtExit = FIRCrashlytics.lastRecordedExceptionModel != nil;
143
+ [self appendEvent:@"exit"];
144
+ [FIRCrashlytics fulfillContextInit];
145
+ [terminated fulfill];
146
+ }
147
+ terminateQueue:dispatch_get_main_queue()
148
+ watchdogQueue:watchdogQueue
149
+ watchdogTimeout:0.2];
150
+
151
+ __block BOOL started = NO;
152
+ dispatch_queue_t moduleQueue =
153
+ dispatch_queue_create("rnfb.crashlytics.test.module", DISPATCH_QUEUE_SERIAL);
154
+ dispatch_async(moduleQueue, ^{
155
+ started = [fatalExit recordThenTerminate:^{
156
+ [self recordFatal];
157
+ }];
158
+ });
159
+
160
+ [self waitForExpectations:@[ terminated ] timeout:5];
161
+ dispatch_resume(watchdogQueue);
162
+ [self drainFor:0.4];
163
+ [self drainQueue:watchdogQueue];
164
+ [self drainQueue:moduleQueue];
165
+
166
+ XCTAssertTrue(started);
167
+ XCTAssertEqual(terminations, 1u);
168
+ XCTAssertTrue(recordedAtExit);
169
+ XCTAssertEqual(savedAtExit, 0u);
170
+ NSArray *expected = @[ @"exit", @"save:fatal" ];
171
+ XCTAssertEqualObjects([self eventsSnapshot], expected);
172
+ }
173
+
174
+ - (void)testWatchdog_terminatesWhenTerminateQueueIsHung {
175
+ dispatch_queue_t hungQueue = dispatch_queue_create("rnfb.crashlytics.test.hung", 0);
176
+ dispatch_semaphore_t release = dispatch_semaphore_create(0);
177
+ dispatch_async(hungQueue, ^{
178
+ dispatch_semaphore_wait(release, DISPATCH_TIME_FOREVER);
179
+ });
180
+
181
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
182
+ __block NSUInteger terminations = 0;
183
+ __block BOOL terminatedWhileHung = NO;
184
+ __block BOOL hung = YES;
185
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
186
+ initWithTerminator:^{
187
+ terminations += 1;
188
+ terminatedWhileHung = hung;
189
+ [terminated fulfill];
190
+ }
191
+ terminateQueue:hungQueue
192
+ watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
193
+ watchdogTimeout:0.1];
194
+
195
+ XCTAssertTrue([fatalExit recordThenTerminate:^{
196
+ [self recordFatal];
197
+ }]);
198
+ [self waitForExpectations:@[ terminated ] timeout:5];
199
+
200
+ hung = NO;
201
+ dispatch_semaphore_signal(release);
202
+ [self drainQueue:hungQueue]; // the queued terminate block has now run
203
+
204
+ XCTAssertTrue(terminatedWhileHung);
205
+ XCTAssertEqual(terminations, 1u);
206
+ }
207
+
208
+ - (void)testRecordRaises_stillTerminatesAndIgnoresLaterFatals {
209
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
210
+ __block NSUInteger terminations = 0;
211
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
212
+ initWithTerminator:^{
213
+ terminations += 1;
214
+ [terminated fulfill];
215
+ }
216
+ terminateQueue:dispatch_get_main_queue()
217
+ watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
218
+ watchdogTimeout:0.1];
219
+
220
+ BOOL first = [fatalExit recordThenTerminate:^{
221
+ [NSException raise:NSInternalInconsistencyException format:@"record failed"];
222
+ }];
223
+ BOOL second = [fatalExit recordThenTerminate:^{
224
+ [self recordFatal];
225
+ }];
226
+
227
+ [self waitForExpectations:@[ terminated ] timeout:5];
228
+ [self drainFor:0.3];
229
+
230
+ XCTAssertTrue(first);
231
+ XCTAssertFalse(second);
232
+ XCTAssertEqual(terminations, 1u);
233
+ XCTAssertEqual(FIRCrashlytics.savedExceptionModels.count, 0u);
234
+ }
235
+
236
+ - (void)testSecondFatalWhileTerminating_isIgnored {
237
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
238
+ __block NSUInteger terminations = 0;
239
+ __block NSUInteger records = 0;
240
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
241
+ initWithTerminator:^{
242
+ terminations += 1;
243
+ [terminated fulfill];
244
+ }
245
+ terminateQueue:dispatch_get_main_queue()
246
+ watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
247
+ watchdogTimeout:0.1];
248
+
249
+ BOOL first = [fatalExit recordThenTerminate:^{
250
+ records += 1;
251
+ [self recordFatal];
252
+ }];
253
+ BOOL second = [fatalExit recordThenTerminate:^{
254
+ records += 1;
255
+ [self recordFatal];
256
+ }];
257
+
258
+ [self waitForExpectations:@[ terminated ] timeout:5];
259
+ [self drainFor:0.3];
260
+
261
+ XCTAssertTrue(first);
262
+ XCTAssertFalse(second);
263
+ XCTAssertEqual(records, 1u);
264
+ XCTAssertEqual(terminations, 1u);
265
+ XCTAssertEqual(FIRCrashlytics.savedExceptionModels.count, 1u);
266
+ }
267
+
268
+ @end
@@ -13,6 +13,7 @@
13
13
  CL1000000000000000000020 /* RNFBCrashlyticsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CL1000000000000000000021 /* RNFBCrashlyticsHelper.m */; };
14
14
  CL1000000000000000000022 /* RNFBCrashlyticsHelperHostStubTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CL1000000000000000000023 /* RNFBCrashlyticsHelperHostStubTests.m */; };
15
15
  CL1000000000000000000024 /* FIRCrashlyticsHostStub.m in Sources */ = {isa = PBXBuildFile; fileRef = CL1000000000000000000025 /* FIRCrashlyticsHostStub.m */; };
16
+ CL1000000000000000000027 /* RNFBCrashlyticsFatalExitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CL1000000000000000000028 /* RNFBCrashlyticsFatalExitTests.m */; };
16
17
  /* End PBXBuildFile section */
17
18
 
18
19
  /* Begin PBXFileReference section */
@@ -23,6 +24,7 @@
23
24
  CL1000000000000000000021 /* RNFBCrashlyticsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNFBCrashlyticsHelper.m; path = ../RNFBCrashlytics/RNFBCrashlyticsHelper.m; sourceTree = SOURCE_ROOT; };
24
25
  CL1000000000000000000023 /* RNFBCrashlyticsHelperHostStubTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNFBCrashlyticsHelperHostStubTests.m; sourceTree = "<group>"; };
25
26
  CL1000000000000000000025 /* FIRCrashlyticsHostStub.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRCrashlyticsHostStub.m; path = HostStubs/Firebase/FIRCrashlyticsHostStub.m; sourceTree = "<group>"; };
27
+ CL1000000000000000000028 /* RNFBCrashlyticsFatalExitTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNFBCrashlyticsFatalExitTests.m; sourceTree = "<group>"; };
26
28
  CL1000000000000000000026 /* RNFBCrashlyticsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNFBCrashlyticsHelper.h; path = ../RNFBCrashlytics/RNFBCrashlyticsHelper.h; sourceTree = SOURCE_ROOT; };
27
29
  /* End PBXFileReference section */
28
30
 
@@ -62,6 +64,7 @@
62
64
  CL1000000000000000000026 /* RNFBCrashlyticsHelper.h */,
63
65
  CL1000000000000000000021 /* RNFBCrashlyticsHelper.m */,
64
66
  CL1000000000000000000023 /* RNFBCrashlyticsHelperHostStubTests.m */,
67
+ CL1000000000000000000028 /* RNFBCrashlyticsFatalExitTests.m */,
65
68
  CL1000000000000000000025 /* FIRCrashlyticsHostStub.m */,
66
69
  );
67
70
  name = Sources;
@@ -129,6 +132,7 @@
129
132
  CL1000000000000000000020 /* RNFBCrashlyticsHelper.m in Sources */,
130
133
  CL1000000000000000000024 /* FIRCrashlyticsHostStub.m in Sources */,
131
134
  CL1000000000000000000022 /* RNFBCrashlyticsHelperHostStubTests.m in Sources */,
135
+ CL1000000000000000000027 /* RNFBCrashlyticsFatalExitTests.m in Sources */,
132
136
  );
133
137
  runOnlyForDeploymentPostprocessing = 0;
134
138
  };
package/ios_config.sh CHANGED
@@ -16,6 +16,16 @@
16
16
  #
17
17
  set -e
18
18
 
19
+ # upload-symbols talks to the network and has no timeout of its own, so a stalled
20
+ # request can hang the whole build. perl's alarm is used because macOS has no
21
+ # `timeout`. A timeout exits non-zero, which the callers below report as a
22
+ # warning without failing the build.
23
+ UPLOAD_SYMBOLS_TIMEOUT_SEC="${RNFB_CRASHLYTICS_UPLOAD_TIMEOUT_SEC:-300}"
24
+
25
+ run_upload_symbols() {
26
+ perl -e 'alarm shift; exec @ARGV' "${UPLOAD_SYMBOLS_TIMEOUT_SEC}" "$@"
27
+ }
28
+
19
29
  if [[ ${PODS_ROOT} && -f "${PODS_ROOT}/FirebaseCrashlytics/run" ]]; then
20
30
  echo "info: Exec FirebaseCrashlytics Run from Pods"
21
31
  if ! "${PODS_ROOT}/FirebaseCrashlytics/run"; then
@@ -49,14 +59,14 @@ else
49
59
  echo "warning: GoogleService-Info.plist not found at ${PROJECT_DIR}/${TARGET_NAME}/GoogleService-Info.plist or ${PROJECT_DIR}/GoogleService-Info.plist. Skipping dSYM upload."
50
60
  elif [[ -x "${SPM_UPLOAD_SYMBOLS}" ]]; then
51
61
  echo "info: Exec FirebaseCrashlytics upload-symbols from SPM"
52
- if ! "${SPM_UPLOAD_SYMBOLS}" -gsp "${GSP_PATH}" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"; then
53
- echo "warning: FirebaseCrashlytics upload-symbols (SPM) failed; continuing without failing the build. Common causes: no network, invalid/placeholder GoogleService-Info.plist credentials, or Firebase project mismatch."
62
+ if ! run_upload_symbols "${SPM_UPLOAD_SYMBOLS}" -gsp "${GSP_PATH}" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"; then
63
+ echo "warning: FirebaseCrashlytics upload-symbols (SPM) failed or timed out; continuing without failing the build. Common causes: no network, invalid/placeholder GoogleService-Info.plist credentials, or Firebase project mismatch."
54
64
  fi
55
65
  elif [[ -f "${SPM_UPLOAD_SYMBOLS}" ]]; then
56
66
  echo "info: Exec FirebaseCrashlytics upload-symbols from SPM (chmod +x)"
57
67
  chmod +x "${SPM_UPLOAD_SYMBOLS}"
58
- if ! "${SPM_UPLOAD_SYMBOLS}" -gsp "${GSP_PATH}" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"; then
59
- echo "warning: FirebaseCrashlytics upload-symbols (SPM) failed; continuing without failing the build. Common causes: no network, invalid/placeholder GoogleService-Info.plist credentials, or Firebase project mismatch."
68
+ if ! run_upload_symbols "${SPM_UPLOAD_SYMBOLS}" -gsp "${GSP_PATH}" -p ios "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}"; then
69
+ echo "warning: FirebaseCrashlytics upload-symbols (SPM) failed or timed out; continuing without failing the build. Common causes: no network, invalid/placeholder GoogleService-Info.plist credentials, or Firebase project mismatch."
60
70
  fi
61
71
  else
62
72
  echo "warning: FirebaseCrashlytics run script not found at CocoaPods, framework, or SPM paths. Skipping dSYM upload."
package/lib/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '27.0.0';
2
+ export const version = '27.1.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-firebase/crashlytics",
3
- "version": "27.0.0",
3
+ "version": "27.1.0",
4
4
  "author": "Invertase <oss@invertase.io> (http://invertase.io)",
5
5
  "description": "React Native Firebase - Firebase Crashlytics is a lightweight, realtime crash reporter that helps you track, prioritize, and fix stability issues that erode your app quality. React Native Firebase provides automatic crash reporting for both native and JavaScript errors, including unhandled promise rejections.",
6
6
  "main": "./dist/module/index.js",
@@ -49,14 +49,14 @@
49
49
  "crashlytics"
50
50
  ],
51
51
  "peerDependencies": {
52
- "@react-native-firebase/app": "27.0.0",
52
+ "@react-native-firebase/app": "27.1.0",
53
53
  "expo": ">=47.0.0"
54
54
  },
55
55
  "dependencies": {
56
56
  "stacktrace-js": "^2.0.2"
57
57
  },
58
58
  "devDependencies": {
59
- "@react-native-firebase/app": "27.0.0",
59
+ "@react-native-firebase/app": "27.1.0",
60
60
  "expo": "^55.0.18",
61
61
  "react-native-builder-bob": "^0.41.0",
62
62
  "typescript": "^6.0.3"
@@ -104,5 +104,5 @@
104
104
  ]
105
105
  ]
106
106
  },
107
- "gitHead": "0c7e942bca361f1765abaf233bf447d795718eff"
107
+ "gitHead": "c65e1ee3b78bcd0c5b66996100d7d1e579ca1614"
108
108
  }