@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [27.1.0](https://github.com/invertase/react-native-firebase/compare/v27.0.0...v27.1.0) (2026-10-23)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **crashlytics, ios:** time out SPM upload-symbols so a stalled upload cannot hang the build ([1cdb7de](https://github.com/invertase/react-native-firebase/commit/1cdb7de24ef5ca12a24cf599cce11d3d8797cbb2))
11
+ - **crashlytics:** reach Android Crashlytics internals by reflection only ([c3f15e3](https://github.com/invertase/react-native-firebase/commit/c3f15e37b1f53e0a81469183e896003ea6e80141))
12
+ - **crashlytics:** save JS fatal report before exiting when chaining is disabled ([cfd00b6](https://github.com/invertase/react-native-firebase/commit/cfd00b65754186aad0f765f61a5db3f19e84b716))
13
+
14
+ ## [27.0.0](https://github.com/invertase/react-native-firebase/compare/v26.4.0...v27.0.0) (2026-10-19)
15
+
16
+ ### ⚠ BREAKING CHANGES
17
+
18
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13
19
+
20
+ ### Features
21
+
22
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13 ([0ca03d1](https://github.com/invertase/react-native-firebase/commit/0ca03d11b0c271fb37cfbee8a0c3074a21969f0b))
23
+ - **test-expo:** add Expo Router example with a route per package ([#9313](https://github.com/invertase/react-native-firebase/issues/9313)) ([50a946a](https://github.com/invertase/react-native-firebase/commit/50a946ab4f6c11670890940b55e8bb71f6332dcf))
24
+
25
+ ### Bug Fixes
26
+
27
+ - **crashlytics, ios:** resolve target-folder GoogleService-Info.plist for SPM dSYM upload ([a2b7e57](https://github.com/invertase/react-native-firebase/commit/a2b7e5789b6df07d3aa2729f734582de6a5feb92)), closes [#9304](https://github.com/invertase/react-native-firebase/issues/9304)
28
+
6
29
  ## [26.4.0](https://github.com/invertase/react-native-firebase/compare/v26.3.3...v26.4.0) (2026-09-05)
7
30
 
8
31
  **Note:** Version bump only for package @react-native-firebase/crashlytics
@@ -32,8 +32,14 @@ Pod::Spec.new do |s|
32
32
  s.tvos.deployment_target = firebase_tvos_target
33
33
  s.source_files = 'ios/**/*.{h,m,mm,cpp,swift}'
34
34
  s.private_header_files = "ios/**/*.h"
35
- s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
35
+ # CocoaPods FileList uses FNM_PATHNAME: `ios/*UnitTests/**` does not match nested
36
+ # files under HostStubs; use `ios/*UnitTests/**/*` (same as sibling packages).
37
+ s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**/*'
38
+ s.swift_version = '5.10'
39
+
40
+ # DEFINES_MODULE is required for the generated RNFBCrashlytics-Swift.h interface.
36
41
  s.pod_target_xcconfig = {
42
+ "DEFINES_MODULE" => "YES",
37
43
  "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBCrashlyticsTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
38
44
  "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES" => "YES",
39
45
  }
@@ -13,6 +13,7 @@ buildscript {
13
13
 
14
14
  dependencies {
15
15
  classpath("com.android.tools.build:gradle:8.4.0")
16
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${project.findProperty('kotlinVersion') ?: '2.4.0'}")
16
17
  }
17
18
  }
18
19
  }
@@ -21,6 +22,8 @@ plugins {
21
22
  id "io.invertase.gradle.build" version "1.5"
22
23
  }
23
24
 
25
+ apply plugin: "org.jetbrains.kotlin.android"
26
+
24
27
  def appProject
25
28
  if (findProject(':@react-native-firebase_app')) {
26
29
  appProject = project(':@react-native-firebase_app')
@@ -65,6 +68,7 @@ android {
65
68
 
66
69
  defaultConfig {
67
70
  multiDexEnabled = true
71
+ consumerProguardFiles 'proguard-rules.pro'
68
72
  }
69
73
 
70
74
  buildFeatures {
@@ -99,6 +103,10 @@ dependencies {
99
103
  implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}")
100
104
  implementation "com.google.firebase:firebase-crashlytics"
101
105
  implementation "com.google.firebase:firebase-crashlytics-ndk"
106
+
107
+ testImplementation "junit:junit:4.13.2"
108
+ testImplementation "org.robolectric:robolectric:4.14.1"
109
+ testImplementation "org.mockito:mockito-core:5.14.2"
102
110
  }
103
111
 
104
112
  ReactNative.shared.applyPackageVersion()
@@ -0,0 +1,20 @@
1
+ # Consumer R8/ProGuard rules for @react-native-firebase/crashlytics.
2
+ #
3
+ # io.invertase.firebase.crashlytics.ReactNativeFirebaseCrashlyticsInternal reaches these Crashlytics
4
+ # SDK internals by reflection to wait for a JS fatal's event write before the process exits. Keep
5
+ # this list in sync with the names there. A missing rule (or an SDK without these members) only
6
+ # makes the barrier fail, which falls back to a bounded wait; it never crashes.
7
+ -keepclassmembers class com.google.firebase.crashlytics.FirebaseCrashlytics {
8
+ com.google.firebase.crashlytics.internal.common.CrashlyticsCore core;
9
+ }
10
+ -keepclassmembers class com.google.firebase.crashlytics.internal.common.CrashlyticsCore {
11
+ com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers crashlyticsWorkers;
12
+ }
13
+ -keepclassmembers class com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers {
14
+ com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorker common;
15
+ com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorker diskWrite;
16
+ }
17
+ -keepclassmembers class com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorker {
18
+ com.google.android.gms.tasks.Task submit(java.lang.Runnable);
19
+ com.google.android.gms.tasks.Task submitTask(java.util.concurrent.Callable);
20
+ }
@@ -0,0 +1,42 @@
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
+ /**
21
+ * Preference / meta / JSON keys for Crashlytics init. Historical Java shape was a
22
+ * package-private class of `static final String` fields; Kotlin [object] + `@JvmField`
23
+ * keeps `import static …Constants.*` and `Constants.KEY_…` working for same-module Java.
24
+ */
25
+ internal object Constants {
26
+ @JvmField
27
+ val KEY_CRASHLYTICS_NDK_ENABLED: String = "crashlytics_ndk_enabled"
28
+
29
+ @JvmField
30
+ val KEY_CRASHLYTICS_DEBUG_ENABLED: String = "crashlytics_debug_enabled"
31
+
32
+ @JvmField
33
+ val KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED: String = "crashlytics_auto_collection_enabled"
34
+
35
+ @JvmField
36
+ val KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED: String =
37
+ "crashlytics_is_error_generation_on_js_crash_enabled"
38
+
39
+ @JvmField
40
+ val KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED: String =
41
+ "crashlytics_javascript_exception_handler_chaining_enabled"
42
+ }
@@ -1,4 +1,4 @@
1
- package io.invertase.firebase.crashlytics;
1
+ package io.invertase.firebase.crashlytics
2
2
 
3
3
  /*
4
4
  * Copyright (c) 2016-present Invertase Limited & Contributors
@@ -21,8 +21,6 @@ package io.invertase.firebase.crashlytics;
21
21
  * This class is purely cosmetic - to indicate on the Crashlytics console that it's a JavaScript
22
22
  * error rather than the generic `java.lang.Exception`.
23
23
  */
24
- class JavaScriptError extends Exception {
25
- JavaScriptError(String message) {
26
- super(message);
27
- }
28
- }
24
+ internal class JavaScriptError(
25
+ message: String,
26
+ ) : Exception(message)
@@ -35,6 +35,9 @@ import java.util.Objects;
35
35
 
36
36
  public class NativeRNFBTurboCrashlytics extends NativeRNFBTurboCrashlyticsSpec {
37
37
  private static final String TAG = "Crashlytics";
38
+ // Process-wide so a second JS fatal during the bounded wait neither records nor exits again.
39
+ private static final ReactNativeFirebaseCrashlyticsFatalExit FATAL_EXIT =
40
+ ReactNativeFirebaseCrashlyticsFatalExit.create();
38
41
 
39
42
  public NativeRNFBTurboCrashlytics(ReactApplicationContext reactContext) {
40
43
  super(reactContext);
@@ -83,10 +86,14 @@ public class NativeRNFBTurboCrashlytics extends NativeRNFBTurboCrashlyticsSpec {
83
86
  @Override
84
87
  public void crashWithStackPromise(ReadableMap jsErrorMap, Promise promise) {
85
88
  if (ReactNativeFirebaseCrashlyticsInitProvider.isCrashlyticsCollectionEnabled()) {
86
- Exception e = recordJavaScriptError(jsErrorMap);
87
- e.printStackTrace(System.err);
88
- Log.e(TAG, "Crash logged. Terminating app.");
89
- System.exit(0);
89
+ // Coverage exception: this branch ends the process, so neither JVM tests nor e2e can
90
+ // observe it. Keep it a thin delegate; barrier, timeout, fallback and the second-fatal guard
91
+ // live in ReactNativeFirebaseCrashlyticsFatalExit (unit-tested). The promise is
92
+ // intentionally never resolved: that keeps the JS global handler suspended (no chaining to
93
+ // the previous handler, no fall-through) until termination.
94
+ Log.e(TAG, "Logging crash and terminating app.");
95
+ FATAL_EXIT.recordThenTerminate(() -> recordJavaScriptError(jsErrorMap).printStackTrace());
96
+ return;
90
97
  } else {
91
98
  Log.i(TAG, "crashlytics collection is not enabled, not crashing.");
92
99
  }
@@ -0,0 +1,117 @@
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 com.google.android.gms.tasks.Tasks;
21
+ import com.google.firebase.crashlytics.FirebaseCrashlytics;
22
+ import java.util.concurrent.TimeUnit;
23
+ import java.util.concurrent.atomic.AtomicBoolean;
24
+
25
+ /**
26
+ * Records a JS fatal, then terminates the process once the record has been written.
27
+ *
28
+ * <p>{@code FirebaseCrashlytics.recordException} writes asynchronously on SDK worker threads, so
29
+ * exiting right after it usually loses the event. This waits on a barrier that completes after the
30
+ * queued event write, bounded by {@link #TIMEOUT_MS}. On any barrier failure (SDK internals
31
+ * changed, timeout, interruption) it waits out the rest of the bound, then terminates anyway.
32
+ *
33
+ * <p>Must run on a background thread (the TurboModule's native-modules thread), never main.
34
+ */
35
+ final class ReactNativeFirebaseCrashlyticsFatalExit {
36
+ static final long TIMEOUT_MS = 2000;
37
+
38
+ interface Barrier {
39
+ void await(long timeoutMs) throws Exception;
40
+ }
41
+
42
+ interface Sleeper {
43
+ void sleep(long millis) throws InterruptedException;
44
+ }
45
+
46
+ interface Clock {
47
+ long nowMillis();
48
+ }
49
+
50
+ final Barrier barrier;
51
+ final Sleeper sleeper;
52
+ final Clock clock;
53
+ final Runnable terminator;
54
+ final long timeoutMs;
55
+ private final AtomicBoolean exiting = new AtomicBoolean(false);
56
+
57
+ ReactNativeFirebaseCrashlyticsFatalExit(
58
+ Barrier barrier, Sleeper sleeper, Clock clock, Runnable terminator, long timeoutMs) {
59
+ this.barrier = barrier;
60
+ this.sleeper = sleeper;
61
+ this.clock = clock;
62
+ this.terminator = terminator;
63
+ this.timeoutMs = timeoutMs;
64
+ }
65
+
66
+ static ReactNativeFirebaseCrashlyticsFatalExit create() {
67
+ return new ReactNativeFirebaseCrashlyticsFatalExit(
68
+ timeoutMs ->
69
+ Tasks.await(
70
+ ReactNativeFirebaseCrashlyticsInternal.pendingEventWritesBarrier(
71
+ FirebaseCrashlytics.getInstance()),
72
+ timeoutMs,
73
+ TimeUnit.MILLISECONDS),
74
+ Thread::sleep,
75
+ () -> TimeUnit.NANOSECONDS.toMillis(System.nanoTime()),
76
+ // Coverage exception: ends the process, so no test can run it.
77
+ () -> System.exit(0),
78
+ TIMEOUT_MS);
79
+ }
80
+
81
+ /**
82
+ * Runs {@code record}, waits (bounded) for it to be written, then terminates, even when {@code
83
+ * record} throws. Returns false without running {@code record} when an earlier call already
84
+ * started terminating.
85
+ */
86
+ boolean recordThenTerminate(Runnable record) {
87
+ if (!exiting.compareAndSet(false, true)) {
88
+ return false;
89
+ }
90
+ try {
91
+ record.run();
92
+ } catch (Throwable recordFailed) {
93
+ // The guard is already set, so terminate anyway rather than leave the app running with
94
+ // every later fatal ignored.
95
+ recordFailed.printStackTrace();
96
+ }
97
+ long deadline = clock.nowMillis() + timeoutMs;
98
+ try {
99
+ barrier.await(timeoutMs);
100
+ } catch (Throwable barrierUnavailable) {
101
+ waitUntil(deadline);
102
+ }
103
+ terminator.run();
104
+ return true;
105
+ }
106
+
107
+ private void waitUntil(long deadline) {
108
+ long remaining;
109
+ while ((remaining = deadline - clock.nowMillis()) > 0) {
110
+ try {
111
+ sleeper.sleep(remaining);
112
+ } catch (InterruptedException ignored) {
113
+ // Keep waiting out the bound; the process terminates right after.
114
+ }
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,80 @@
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 com.google.android.gms.tasks.Task;
21
+ import com.google.firebase.crashlytics.FirebaseCrashlytics;
22
+ import java.lang.reflect.Field;
23
+ import java.lang.reflect.Method;
24
+ import java.util.Objects;
25
+ import java.util.concurrent.Callable;
26
+
27
+ /**
28
+ * Reaches Firebase Crashlytics SDK internals that have no public API.
29
+ *
30
+ * <p>{@code recordException} queues its event on the {@code common} worker, which then queues the
31
+ * event write on the {@code diskWrite} worker. Both workers are serial, so a {@code common} task
32
+ * that itself queues a no-op on {@code diskWrite} completes only after every event write already
33
+ * queued by earlier {@code recordException} calls.
34
+ *
35
+ * <p>Apps can override the Firebase BoM, so every internal member is reached by reflection: an SDK
36
+ * without them still builds, and any mismatch throws at runtime. Callers must treat every failure
37
+ * as "barrier unavailable". The reflected members are kept by the package's consumer R8/ProGuard
38
+ * rules ({@code proguard-rules.pro}); keep both lists in sync.
39
+ */
40
+ final class ReactNativeFirebaseCrashlyticsInternal {
41
+ static final String CORE_FIELD = "core";
42
+ static final String WORKERS_FIELD = "crashlyticsWorkers";
43
+ static final String COMMON_FIELD = "common";
44
+ static final String DISK_WRITE_FIELD = "diskWrite";
45
+ static final String SUBMIT_TASK_METHOD = "submitTask";
46
+ static final String SUBMIT_METHOD = "submit";
47
+
48
+ private ReactNativeFirebaseCrashlyticsInternal() {}
49
+
50
+ /** Returns a task that completes after all event writes already queued by the SDK. */
51
+ static Task<?> pendingEventWritesBarrier(FirebaseCrashlytics crashlytics)
52
+ throws ReflectiveOperationException {
53
+ return barrierOn(read(read(crashlytics, CORE_FIELD), WORKERS_FIELD));
54
+ }
55
+
56
+ /** {@code workers.common.submitTask(() -> workers.diskWrite.submit(() -> {}))}, reflectively. */
57
+ static Task<?> barrierOn(Object workers) throws ReflectiveOperationException {
58
+ Object common = read(workers, COMMON_FIELD);
59
+ Object diskWrite = read(workers, DISK_WRITE_FIELD);
60
+ Method submit = diskWrite.getClass().getMethod(SUBMIT_METHOD, Runnable.class);
61
+ Method submitTask = common.getClass().getMethod(SUBMIT_TASK_METHOD, Callable.class);
62
+ Runnable noop = () -> {};
63
+ Callable<Object> queueNoopOnDiskWrite = () -> submit.invoke(diskWrite, noop);
64
+ return (Task<?>) submitTask.invoke(common, queueNoopOnDiskWrite);
65
+ }
66
+
67
+ /** Reads field {@code name} declared on {@code owner}'s class or a superclass; never null. */
68
+ static Object read(Object owner, String name) throws ReflectiveOperationException {
69
+ for (Class<?> type = owner.getClass(); type != null; type = type.getSuperclass()) {
70
+ try {
71
+ Field field = type.getDeclaredField(name);
72
+ field.setAccessible(true);
73
+ return Objects.requireNonNull(field.get(owner), name);
74
+ } catch (NoSuchFieldException notDeclaredHere) {
75
+ // Keep looking up the hierarchy (e.g. a subclass of the SDK type).
76
+ }
77
+ }
78
+ throw new NoSuchFieldException(name);
79
+ }
80
+ }
@@ -0,0 +1,45 @@
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 com.google.firebase.crashlytics.FirebaseCrashlytics
21
+
22
+ /**
23
+ * Public static helpers for native/Android callers that need Crashlytics without going through the
24
+ * TurboModule. Historical Java shape was a class of static methods; Kotlin [object] + `@JvmStatic`
25
+ * keeps `ReactNativeFirebaseCrashlyticsNativeHelper.recordNativeException(...)` working for Java.
26
+ */
27
+ object ReactNativeFirebaseCrashlyticsNativeHelper {
28
+ @JvmStatic
29
+ fun recordNativeException(throwable: Throwable) {
30
+ FirebaseCrashlytics.getInstance().recordException(throwable)
31
+ }
32
+
33
+ @JvmStatic
34
+ fun log(message: String) {
35
+ FirebaseCrashlytics.getInstance().log(message)
36
+ }
37
+
38
+ @JvmStatic
39
+ fun setCustomKey(
40
+ key: String,
41
+ value: String,
42
+ ) {
43
+ FirebaseCrashlytics.getInstance().setCustomKey(key, value)
44
+ }
45
+ }
@@ -1,4 +1,4 @@
1
- package io.invertase.firebase.crashlytics;
1
+ package io.invertase.firebase.crashlytics
2
2
 
3
3
  /*
4
4
  * Copyright (c) 2016-present Invertase Limited & Contributors
@@ -21,8 +21,6 @@ package io.invertase.firebase.crashlytics;
21
21
  * This class is purely cosmetic - to indicate on the Crashlytics console that it's an
22
22
  * UnhandledPromiseRejection JS error rather than the generic `java.lang.Exception`.
23
23
  */
24
- class UnhandledPromiseRejection extends Exception {
25
- UnhandledPromiseRejection(String message) {
26
- super(message);
27
- }
28
- }
24
+ internal class UnhandledPromiseRejection(
25
+ message: String,
26
+ ) : Exception(message)
@@ -0,0 +1,84 @@
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 io.invertase.firebase.crashlytics.Constants.KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED;
21
+ import static io.invertase.firebase.crashlytics.Constants.KEY_CRASHLYTICS_DEBUG_ENABLED;
22
+ import static io.invertase.firebase.crashlytics.Constants.KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED;
23
+ import static io.invertase.firebase.crashlytics.Constants.KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED;
24
+ import static io.invertase.firebase.crashlytics.Constants.KEY_CRASHLYTICS_NDK_ENABLED;
25
+ import static org.junit.Assert.assertEquals;
26
+ import static org.junit.Assert.assertTrue;
27
+
28
+ import java.lang.reflect.Field;
29
+ import java.lang.reflect.Modifier;
30
+ import org.junit.Test;
31
+
32
+ /**
33
+ * JVM coverage for Kotlin-ported {@link Constants}, {@link JavaScriptError}, and {@link
34
+ * UnhandledPromiseRejection}. Asserts preference-key string values, Java static-field shape for
35
+ * InitProvider static imports, and cosmetic Crashlytics console type names.
36
+ */
37
+ public class ConstantsAndJsErrorTypesTest {
38
+
39
+ @Test
40
+ public void constants_stringValuesMatchExpectedKeys() {
41
+ assertEquals("crashlytics_ndk_enabled", KEY_CRASHLYTICS_NDK_ENABLED);
42
+ assertEquals("crashlytics_debug_enabled", KEY_CRASHLYTICS_DEBUG_ENABLED);
43
+ assertEquals("crashlytics_auto_collection_enabled", KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED);
44
+ assertEquals(
45
+ "crashlytics_is_error_generation_on_js_crash_enabled",
46
+ KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED);
47
+ assertEquals(
48
+ "crashlytics_javascript_exception_handler_chaining_enabled",
49
+ KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED);
50
+ }
51
+
52
+ @Test
53
+ public void constants_fieldsArePublicStaticForJavaStaticImport() throws Exception {
54
+ String[] names = {
55
+ "KEY_CRASHLYTICS_NDK_ENABLED",
56
+ "KEY_CRASHLYTICS_DEBUG_ENABLED",
57
+ "KEY_CRASHLYTICS_AUTO_COLLECTION_ENABLED",
58
+ "KEY_CRASHLYTICS_IS_ERROR_GENERATION_ON_JS_CRASH_ENABLED",
59
+ "KEY_CRASHLYTICS_JAVASCRIPT_EXCEPTION_HANDLER_CHAINING_ENABLED",
60
+ };
61
+ for (String name : names) {
62
+ Field field = Constants.class.getField(name);
63
+ int modifiers = field.getModifiers();
64
+ assertTrue(name, Modifier.isPublic(modifiers));
65
+ assertTrue(name, Modifier.isStatic(modifiers));
66
+ assertTrue(name, Modifier.isFinal(modifiers));
67
+ assertEquals(name, String.class, field.getType());
68
+ }
69
+ }
70
+
71
+ @Test
72
+ public void javaScriptError_constructsWithMessageAndCosmeticSimpleName() {
73
+ JavaScriptError error = new JavaScriptError("js-boom");
74
+ assertEquals("js-boom", error.getMessage());
75
+ assertEquals("JavaScriptError", error.getClass().getSimpleName());
76
+ }
77
+
78
+ @Test
79
+ public void unhandledPromiseRejection_constructsWithMessageAndCosmeticSimpleName() {
80
+ UnhandledPromiseRejection error = new UnhandledPromiseRejection("promise-boom");
81
+ assertEquals("promise-boom", error.getMessage());
82
+ assertEquals("UnhandledPromiseRejection", error.getClass().getSimpleName());
83
+ }
84
+ }
@@ -0,0 +1,122 @@
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.assertTrue;
22
+ import static org.mockito.Mockito.mock;
23
+ import static org.mockito.Mockito.mockStatic;
24
+
25
+ import com.google.firebase.crashlytics.FirebaseCrashlytics;
26
+ import com.google.firebase.crashlytics.internal.common.CrashlyticsCore;
27
+ import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers;
28
+ import java.util.ArrayList;
29
+ import java.util.Arrays;
30
+ import java.util.List;
31
+ import java.util.concurrent.ExecutorService;
32
+ import java.util.concurrent.Executors;
33
+ import java.util.concurrent.TimeUnit;
34
+ import java.util.concurrent.atomic.AtomicBoolean;
35
+ import org.junit.After;
36
+ import org.junit.Test;
37
+ import org.junit.runner.RunWith;
38
+ import org.mockito.MockedStatic;
39
+ import org.robolectric.RobolectricTestRunner;
40
+
41
+ /**
42
+ * Covers the production barrier wired by {@link ReactNativeFirebaseCrashlyticsFatalExit#create()}
43
+ * against real Crashlytics workers. Robolectric for the SDK classes; AndroidTest-AD-1. The barrier
44
+ * runs on a pool thread (as on the native-modules thread in production), and {@code mockStatic} is
45
+ * thread-bound, so it is opened on that thread.
46
+ */
47
+ @RunWith(RobolectricTestRunner.class)
48
+ public class ReactNativeFirebaseCrashlyticsFatalExitBarrierTest {
49
+ private final ExecutorService pool = Executors.newCachedThreadPool();
50
+
51
+ @After
52
+ public void tearDown() {
53
+ pool.shutdownNow();
54
+ }
55
+
56
+ @Test
57
+ public void productionBarrier_waitsForTheQueuedEventWrite() throws Exception {
58
+ CrashlyticsWorkers workers = new CrashlyticsWorkers(pool, pool);
59
+ FirebaseCrashlytics crashlytics =
60
+ ReactNativeFirebaseCrashlyticsInternalTest.crashlyticsWith(workers);
61
+ AtomicBoolean eventWritten = new AtomicBoolean(false);
62
+ workers.diskWrite.submit(
63
+ () -> {
64
+ try {
65
+ Thread.sleep(150);
66
+ } catch (InterruptedException e) {
67
+ throw new IllegalStateException(e);
68
+ }
69
+ eventWritten.set(true);
70
+ });
71
+
72
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
73
+ ReactNativeFirebaseCrashlyticsFatalExit.create();
74
+ pool.submit(
75
+ () -> {
76
+ try (MockedStatic<FirebaseCrashlytics> firebaseCrashlytics =
77
+ mockStatic(FirebaseCrashlytics.class)) {
78
+ firebaseCrashlytics.when(FirebaseCrashlytics::getInstance).thenReturn(crashlytics);
79
+ fatalExit.barrier.await(fatalExit.timeoutMs);
80
+ }
81
+ return null;
82
+ })
83
+ .get(10, TimeUnit.SECONDS);
84
+
85
+ assertTrue(eventWritten.get());
86
+ }
87
+
88
+ /**
89
+ * When the barrier is unavailable (here: an unset {@code crashlyticsWorkers}), the production
90
+ * barrier fails and the fatal exit waits out the bound, then terminates.
91
+ */
92
+ @Test
93
+ public void productionBarrier_barrierUnavailable_fallsBackToTheBoundedWait() throws Exception {
94
+ FirebaseCrashlytics crashlytics =
95
+ ReactNativeFirebaseCrashlyticsInternalTest.crashlyticsWithCore(mock(CrashlyticsCore.class));
96
+ List<String> events = new ArrayList<>();
97
+ long[] now = {0};
98
+ ReactNativeFirebaseCrashlyticsFatalExit fatalExit =
99
+ new ReactNativeFirebaseCrashlyticsFatalExit(
100
+ ReactNativeFirebaseCrashlyticsFatalExit.create().barrier,
101
+ millis -> {
102
+ events.add("sleep:" + millis);
103
+ now[0] += millis;
104
+ },
105
+ () -> now[0],
106
+ () -> events.add("exit"),
107
+ ReactNativeFirebaseCrashlyticsFatalExit.TIMEOUT_MS);
108
+
109
+ pool.submit(
110
+ () -> {
111
+ try (MockedStatic<FirebaseCrashlytics> firebaseCrashlytics =
112
+ mockStatic(FirebaseCrashlytics.class)) {
113
+ firebaseCrashlytics.when(FirebaseCrashlytics::getInstance).thenReturn(crashlytics);
114
+ assertTrue(fatalExit.recordThenTerminate(() -> events.add("record")));
115
+ }
116
+ return null;
117
+ })
118
+ .get(10, TimeUnit.SECONDS);
119
+
120
+ assertEquals(Arrays.asList("record", "sleep:2000", "exit"), events);
121
+ }
122
+ }