diff --git a/crashlytics/src/AndroidImpl.cs b/crashlytics/src/AndroidImpl.cs index a74d97e1..0bfd2801 100644 --- a/crashlytics/src/AndroidImpl.cs +++ b/crashlytics/src/AndroidImpl.cs @@ -20,6 +20,7 @@ namespace Firebase.Crashlytics using System; using System.Diagnostics; using System.Collections.Generic; + using System.Linq; using UnityEngine; @@ -141,6 +142,39 @@ public override void LogException(Exception exception) { }, "LogException"); } + public override void LogExceptionAsFatal(Exception exception) { + var loggedException = LoggedException.FromException(exception); + Dictionary[] parsedStackTrace = loggedException.ParsedStackTrace; + + if (parsedStackTrace.Length == 0) { + // if for some reason we don't get stack trace from exception, we add current stack trace in + var currentStackTrace = System.Environment.StackTrace; + LoggedException loggedExceptionWithCurrentStackTrace = new LoggedException(loggedException.Name, loggedException.Message, currentStackTrace); + parsedStackTrace = loggedExceptionWithCurrentStackTrace.ParsedStackTrace; + + if (parsedStackTrace.Length > 3) { + // remove AndroidImpl frames for fault blame on crashlytics sdk + var slicedParsedStackTrace = parsedStackTrace.Skip(3).Take(parsedStackTrace.Length - 3).ToArray(); + parsedStackTrace = slicedParsedStackTrace; + } + } + + StackFrames frames = new StackFrames(); + foreach (Dictionary frame in parsedStackTrace) { + frames.Add(new FirebaseCrashlyticsFrame { + library = frame["class"], + symbol = frame["method"], + fileName = frame["file"], + lineNumber = frame["line"], + }); + } + + CallInternalMethod(() => { + crashlyticsInternal.LogExceptionAsFatal(loggedException.Name, + loggedException.Message, frames); + }, "LogExceptionAsFatal"); + } + public override bool IsCrashlyticsCollectionEnabled() { return CallInternalMethod(() => { return crashlyticsInternal.IsCrashlyticsCollectionEnabled(); diff --git a/crashlytics/src/Crashlytics.cs b/crashlytics/src/Crashlytics.cs index d9048c8d..2501e107 100644 --- a/crashlytics/src/Crashlytics.cs +++ b/crashlytics/src/Crashlytics.cs @@ -28,6 +28,16 @@ namespace Firebase.Crashlytics { [UnityEngine.Scripting.Preserve] public static class Crashlytics { + /// + /// Whether Crashlytics is set to report uncaught exceptions as fatal. + /// Fatal exceptions count towards Crash Free Users and Velocity Alerts. + /// It is recommended to enable this for new apps. + /// + /// true if Crashlytics is set to report uncaught exceptions as fatal, false otherwise. + /// + /// + public static bool ReportUncaughtExceptionsAsFatal { get; set; } = false; + /// /// Checks whether the Crashlytics specific data collection flag has been enabled. /// @@ -106,6 +116,16 @@ public static void LogException(Exception exception) { PlatformAccessor.Impl.LogException(exception); } + /// + /// Record a fatal exception. + /// + /// + /// The exception to log as fatal. + /// + public static void LogExceptionAsFatal(Exception exception) { + PlatformAccessor.Impl.LogExceptionAsFatal(exception); + } + /// /// This class holds a privately held instances that should be accessed via /// the internal getters. This allows us to lazily initialize the instances diff --git a/crashlytics/src/ExceptionHandler.cs b/crashlytics/src/ExceptionHandler.cs index 4174ba22..77bbc5e5 100644 --- a/crashlytics/src/ExceptionHandler.cs +++ b/crashlytics/src/ExceptionHandler.cs @@ -94,7 +94,11 @@ internal virtual void LogException(LoggedException e) { "Exception stack trace:\n" + "{1}", e.Message, e.StackTrace) ); - Crashlytics.LogException(e); + if (Crashlytics.ReportUncaughtExceptionsAsFatal) { + Crashlytics.LogExceptionAsFatal(e); + } else { + Crashlytics.LogException(e); + } } } } diff --git a/crashlytics/src/IOSImpl.cs b/crashlytics/src/IOSImpl.cs index 2144e635..5793fc6b 100644 --- a/crashlytics/src/IOSImpl.cs +++ b/crashlytics/src/IOSImpl.cs @@ -20,6 +20,7 @@ namespace Firebase.Crashlytics using System.Runtime.InteropServices; using System.Diagnostics; using System.Collections.Generic; + using System.Linq; using UnityEngine; @@ -60,7 +61,7 @@ internal class IOSImpl : Impl [DllImport("__Internal")] private static extern void CLURecordCustomException(string name, string reason, - Frame[] frames, int frameCount); + Frame[] frames, int frameCount, bool isOnDemand); [DllImport("__Internal")] private static extern bool CLUIsCrashlyticsCollectionEnabled(); @@ -96,7 +97,12 @@ public override void SetUserId(string identifier) public override void LogException(Exception exception) { var loggedException = LoggedException.FromException(exception); - RecordCustomException(loggedException); + RecordCustomException(loggedException, false); + } + + public override void LogExceptionAsFatal(Exception exception) { + var loggedException = LoggedException.FromException(exception); + RecordCustomException(loggedException, true); } public override bool IsCrashlyticsCollectionEnabled() { @@ -108,9 +114,22 @@ public override void SetCrashlyticsCollectionEnabled(bool enabled) { } // private void RecordCustomException(string name, string reason, string stackTraceString) - private void RecordCustomException(LoggedException loggedException) { + private void RecordCustomException(LoggedException loggedException, bool isOnDemand) { Dictionary[] parsedStackTrace = loggedException.ParsedStackTrace; + if (isOnDemand && parsedStackTrace.Length == 0) { + // if for some reason we don't get stack trace from exception, we add current stack trace in + var currentStackTrace = System.Environment.StackTrace; + LoggedException loggedExceptionWithCurrentStackTrace = new LoggedException(loggedException.Name, loggedException.Message, currentStackTrace); + parsedStackTrace = loggedExceptionWithCurrentStackTrace.ParsedStackTrace; + + if (parsedStackTrace.Length > 2) { + // remove RecordCustomException and System.Environment.StackTrace frame for fault blame on crashlytics sdk + var slicedParsedStackTrace = parsedStackTrace.Skip(2).Take(parsedStackTrace.Length - 2).ToArray(); + parsedStackTrace = slicedParsedStackTrace; + } + } + List frames = new List(); foreach (Dictionary frame in parsedStackTrace) { frames.Add(new Frame { @@ -121,7 +140,7 @@ private void RecordCustomException(LoggedException loggedException) { }); } - CLURecordCustomException(loggedException.Name, loggedException.Message, frames.ToArray(), frames.Count); + CLURecordCustomException(loggedException.Name, loggedException.Message, frames.ToArray(), frames.Count, isOnDemand); } } } diff --git a/crashlytics/src/Impl.cs b/crashlytics/src/Impl.cs index 40302f3d..9d666e63 100644 --- a/crashlytics/src/Impl.cs +++ b/crashlytics/src/Impl.cs @@ -35,6 +35,8 @@ internal class Impl { "Would set user identifier if running on a physical device: {0}"; private static readonly string LogExceptionString = "Would log exception if running on a physical device: {0}"; + private static readonly string LogExceptionAsFatalString = + "Would log exception as fatal if running on a physical device: {0}"; private static readonly string IsCrashlyticsCollectionEnabledString = "Would get Crashlytics data collection if running on a physical device"; private static readonly string SetCrashlyticsCollectionEnabledString = @@ -70,6 +72,10 @@ public virtual void LogException(Exception exception) { LogUtil.LogMessage(LogLevel.Debug, String.Format(LogExceptionString, exception)); } + public virtual void LogExceptionAsFatal(Exception exception) { + LogUtil.LogMessage(LogLevel.Debug, String.Format(LogExceptionAsFatalString, exception)); + } + public virtual bool IsCrashlyticsCollectionEnabled() { LogUtil.LogMessage(LogLevel.Debug, String.Format(IsCrashlyticsCollectionEnabledString)); return true; diff --git a/crashlytics/src/cpp/android/crashlytics_android.cc b/crashlytics/src/cpp/android/crashlytics_android.cc index 8707a26b..da46984f 100644 --- a/crashlytics/src/cpp/android/crashlytics_android.cc +++ b/crashlytics/src/cpp/android/crashlytics_android.cc @@ -62,6 +62,11 @@ METHOD_LOOKUP_DEFINITION(firebase_crashlytics, CRASHLYTICS_METHODS, CRASHLYTICS_FIELDS) // clang-format off +#define CRASHLYTICS_CORE_METHODS(X) \ + X(LogFatalException, "logFatalException", \ + "(Ljava/lang/Throwable;)V", \ + util::kMethodTypeInstance) + #define CRASHLYTICS_CORE_FIELDS(X) \ X(DataCollectionArbiter, "dataCollectionArbiter", \ "Lcom/google/firebase/crashlytics/internal/common/DataCollectionArbiter;", \ @@ -69,12 +74,13 @@ METHOD_LOOKUP_DEFINITION(firebase_crashlytics, // clang-format on METHOD_LOOKUP_DECLARATION(crashlytics_core, - METHOD_LOOKUP_NONE, CRASHLYTICS_CORE_FIELDS) + CRASHLYTICS_CORE_METHODS, + CRASHLYTICS_CORE_FIELDS) METHOD_LOOKUP_DEFINITION( crashlytics_core, PROGUARD_KEEP_CLASS "com/google/firebase/crashlytics/internal/common/CrashlyticsCore", - METHOD_LOOKUP_NONE, CRASHLYTICS_CORE_FIELDS) + CRASHLYTICS_CORE_METHODS, CRASHLYTICS_CORE_FIELDS) // clang-format off #define CRASHLYTICS_DATA_COLLECTION_METHODS(X) \ @@ -144,6 +150,7 @@ bool cached_data_collection_enabled_ = false; CrashlyticsInternal::CrashlyticsInternal(App* app) { data_collection_obj_ = nullptr; + core_ = nullptr; obj_ = nullptr; java_vm_ = app->java_vm(); @@ -188,6 +195,7 @@ CrashlyticsInternal::CrashlyticsInternal(App* app) { env->DeleteLocalRef(application_context); assert(data_collection_obj != nullptr); data_collection_obj_ = env->NewGlobalRef(data_collection_obj); + core_ = env->NewGlobalRef(core); env->DeleteLocalRef(data_collection_obj); env->DeleteLocalRef(core); @@ -212,6 +220,10 @@ CrashlyticsInternal::~CrashlyticsInternal() { env->DeleteGlobalRef(data_collection_obj_); data_collection_obj_ = nullptr; } + if (core_) { + env->DeleteGlobalRef(core_); + core_ = nullptr; + } Terminate(); java_vm_ = nullptr; @@ -227,6 +239,7 @@ bool CrashlyticsInternal::Initialize(JNIEnv* env, jobject activity) { if (!(firebase_crashlytics::CacheMethodIds(env, activity) && firebase_crashlytics::CacheFieldIds(env, activity) && firebase_crashlytics_ndk::CacheMethodIds(env, activity) && + crashlytics_core::CacheMethodIds(env, activity) && crashlytics_core::CacheFieldIds(env, activity) && crashlytics_data_collection::CacheMethodIds(env, activity) && java_exception::CacheMethodIds(env, activity) && @@ -340,6 +353,29 @@ void CrashlyticsInternal::LogException( env->DeleteLocalRef(exception_object); } +void CrashlyticsInternal::LogExceptionAsFatal( + const char* name, const char* reason, + std::vector frames) { + if (!cached_data_collection_enabled_) { + return; + } + JNIEnv* env = util::GetThreadsafeJNIEnv(java_vm_); + + std::string message(name); + message += EXCEPTION_MESSAGE_SEPARATOR; + message += reason; + + jobject exception_object = BuildJavaException(message, frames); + + env->CallVoidMethod( + core_, + crashlytics_core::GetMethodId(crashlytics_core::kLogFatalException), + exception_object); + util::LogException(env, kLogLevelError, + "Crashlytics::LogExceptionAsFatal() failed"); + env->DeleteLocalRef(exception_object); +} + bool CrashlyticsInternal::GetCrashlyticsCollectionEnabled( JavaVM* java_vm, jobject data_collection_obj) { JNIEnv* env = util::GetThreadsafeJNIEnv(java_vm); diff --git a/crashlytics/src/cpp/android/crashlytics_android.h b/crashlytics/src/cpp/android/crashlytics_android.h index e4417ddd..ff7afe18 100644 --- a/crashlytics/src/cpp/android/crashlytics_android.h +++ b/crashlytics/src/cpp/android/crashlytics_android.h @@ -45,6 +45,8 @@ class CrashlyticsInternal { void SetUserId(const char* id); void LogException(const char* name, const char* reason, std::vector frames); + void LogExceptionAsFatal(const char* name, const char* reason, + std::vector frames); bool IsCrashlyticsCollectionEnabled(); void SetCrashlyticsCollectionEnabled(bool enabled); @@ -72,6 +74,9 @@ class CrashlyticsInternal { // Java DataCollectionArbiter global ref. jobject data_collection_obj_; + + // Java CrashlyticsCore global ref. + jobject core_; }; } // namespace internal diff --git a/crashlytics/src/cpp/common/crashlytics.cc b/crashlytics/src/cpp/common/crashlytics.cc index a724e1b7..c62fa2ac 100644 --- a/crashlytics/src/cpp/common/crashlytics.cc +++ b/crashlytics/src/cpp/common/crashlytics.cc @@ -91,6 +91,11 @@ void Crashlytics::LogException(const char* name, const char* reason, internal_->LogException(name, reason, frames); } +void Crashlytics::LogExceptionAsFatal(const char* name, const char* reason, + std::vector frames) { + internal_->LogExceptionAsFatal(name, reason, frames); +} + bool Crashlytics::IsCrashlyticsCollectionEnabled() { return internal_->IsCrashlyticsCollectionEnabled(); } diff --git a/crashlytics/src/cpp/include/firebase/crashlytics.h b/crashlytics/src/cpp/include/firebase/crashlytics.h index 9da5b30c..5f215e83 100644 --- a/crashlytics/src/cpp/include/firebase/crashlytics.h +++ b/crashlytics/src/cpp/include/firebase/crashlytics.h @@ -69,6 +69,8 @@ class Crashlytics { void SetUserId(const char* id); void LogException(const char* name, const char* reason, std::vector frames); + void LogExceptionAsFatal(const char* name, const char* reason, + std::vector frames); bool IsCrashlyticsCollectionEnabled(); void SetCrashlyticsCollectionEnabled(bool enabled); diff --git a/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/Crashlytics_Platform.h b/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/Crashlytics_Platform.h index 09f394f4..b32d23cf 100644 --- a/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/Crashlytics_Platform.h +++ b/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/Crashlytics_Platform.h @@ -25,6 +25,7 @@ @property(nonatomic, strong, nullable) NSString* developmentPlatformName; @property(nonatomic, strong, nullable) NSString* developmentPlatformVersion; +- (void)recordOnDemandExceptionModel:(FIRExceptionModel* _Nonnull)exceptionModel; @end void FIRCLSUserLoggingRecordInternalKeyValue(NSString* key, id value); diff --git a/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/ExceptionModel_Platform.h b/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/ExceptionModel_Platform.h new file mode 100644 index 00000000..605b0db5 --- /dev/null +++ b/crashlytics/src/cpp/ios/Crashlytics_PrivateHeaders/ExceptionModel_Platform.h @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Crashlytics_ExceptionModel.h +// Crashlytics +// + +#import + +@interface FIRExceptionModel (Platform) + +@property(nonatomic) BOOL isFatal; +@property(nonatomic) BOOL onDemand; + +@end \ No newline at end of file diff --git a/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.h b/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.h index 0903a3d9..57c18879 100644 --- a/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.h +++ b/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.h @@ -40,7 +40,7 @@ typedef struct Frame { // to used to log NSException objects. All safely-reportable NSExceptions are // automatically captured by Crashlytics. void CLURecordCustomException(const char *name, const char *reason, - Frame *frames, int frameCount); + Frame *frames, int frameCount, bool isOnDemand); // Returns true when the Crashlytics SDK is initialized. bool CLUIsInitialized(); diff --git a/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.m b/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.m index 15ec34a5..4ad645d1 100644 --- a/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.m +++ b/crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.m @@ -21,6 +21,7 @@ #import "CrashlyticsiOSWrapper.h" #import "FirebaseCrashlytics.h" #import "./Crashlytics_PrivateHeaders/Crashlytics_Platform.h" +#import "./Crashlytics_PrivateHeaders/ExceptionModel_Platform.h" @interface FIRCrashlytics () - (BOOL)isCrashlyticsStarted; @@ -38,7 +39,7 @@ - (BOOL)isCrashlyticsStarted; #pragma mark Main API -void CLURecordCustomException(const char *name, const char *reason, Frame *frames, int frameCount) { +void CLURecordCustomException(const char *name, const char *reason, Frame *frames, int frameCount, bool isOnDemand) { NSString *nameString = safeCharToNSString(name); NSString *reasonString = safeCharToNSString(reason); NSMutableArray *framesArray = [NSMutableArray arrayWithCapacity:frameCount]; @@ -57,6 +58,13 @@ void CLURecordCustomException(const char *name, const char *reason, Frame *frame [FIRExceptionModel exceptionModelWithName:nameString reason:reasonString]; model.stackTrace = framesArray; + if (isOnDemand) { + // For on demand exception, we log them as fatal + model.onDemand = YES; + model.isFatal = YES; + [[FIRCrashlytics crashlytics] recordOnDemandExceptionModel:model]; + return; + } [[FIRCrashlytics crashlytics] recordExceptionModel:model]; } diff --git a/crashlytics/src/cpp/stub/crashlytics_stub.cc b/crashlytics/src/cpp/stub/crashlytics_stub.cc index 997f25fa..6d6f9ba9 100644 --- a/crashlytics/src/cpp/stub/crashlytics_stub.cc +++ b/crashlytics/src/cpp/stub/crashlytics_stub.cc @@ -41,6 +41,10 @@ void CrashlyticsInternal::LogException( const char* name, const char* reason, std::vector frames) {} +void CrashlyticsInternal::LogExceptionAsFatal( + const char* name, const char* reason, + std::vector frames) {} + bool CrashlyticsInternal::IsCrashlyticsCollectionEnabled() { return false; } void CrashlyticsInternal::SetCrashlyticsCollectionEnabled(bool enabled) {} diff --git a/crashlytics/src/cpp/stub/crashlytics_stub.h b/crashlytics/src/cpp/stub/crashlytics_stub.h index 948a85fa..640033fc 100644 --- a/crashlytics/src/cpp/stub/crashlytics_stub.h +++ b/crashlytics/src/cpp/stub/crashlytics_stub.h @@ -43,6 +43,8 @@ class CrashlyticsInternal { void SetUserId(const char* id); void LogException(const char* name, const char* reason, std::vector frames); + void LogExceptionAsFatal(const char* name, const char* reason, + std::vector frames); bool IsCrashlyticsCollectionEnabled(); void SetCrashlyticsCollectionEnabled(bool enabled);