@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
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
|
package/RNFBCrashlytics.podspec
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
}
|
|
24
|
+
internal class JavaScriptError(
|
|
25
|
+
message: String,
|
|
26
|
+
) : Exception(message)
|
package/android/src/main/java/io/invertase/firebase/crashlytics/NativeRNFBTurboCrashlytics.java
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
}
|
|
24
|
+
internal class UnhandledPromiseRejection(
|
|
25
|
+
message: String,
|
|
26
|
+
) : Exception(message)
|
package/android/src/test/java/io/invertase/firebase/crashlytics/ConstantsAndJsErrorTypesTest.java
ADDED
|
@@ -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
|
+
}
|