Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions crashlytics/src/AndroidImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Firebase.Crashlytics
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;

using UnityEngine;

Expand Down Expand Up @@ -141,6 +142,39 @@ public override void LogException(Exception exception) {
}, "LogException");
}

public override void LogExceptionAsFatal(Exception exception) {
var loggedException = LoggedException.FromException(exception);
Dictionary<string, string>[] 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<string, string> 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();
Expand Down
20 changes: 20 additions & 0 deletions crashlytics/src/Crashlytics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ namespace Firebase.Crashlytics {
[UnityEngine.Scripting.Preserve]
public static class Crashlytics {

/// <summary>
/// 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.
/// <returns>
/// true if Crashlytics is set to report uncaught exceptions as fatal, false otherwise.
/// </returns>
/// </summary>
public static bool ReportUncaughtExceptionsAsFatal { get; set; } = false;

/// <summary>
/// Checks whether the Crashlytics specific data collection flag has been enabled.
/// <returns>
Expand Down Expand Up @@ -106,6 +116,16 @@ public static void LogException(Exception exception) {
PlatformAccessor.Impl.LogException(exception);
}

/// <summary>
/// Record a fatal exception.
/// </summary>
/// <param name="exception">
/// The exception to log as fatal.
/// </param>
public static void LogExceptionAsFatal(Exception exception) {
PlatformAccessor.Impl.LogExceptionAsFatal(exception);
}

/// <summary>
/// This class holds a privately held instances that should be accessed via
/// the internal getters. This allows us to lazily initialize the instances
Expand Down
6 changes: 5 additions & 1 deletion crashlytics/src/ExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
27 changes: 23 additions & 4 deletions crashlytics/src/IOSImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Firebase.Crashlytics
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;


Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() {
Expand All @@ -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<string, string>[] 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<Frame> frames = new List<Frame>();
foreach (Dictionary<string, string> frame in parsedStackTrace) {
frames.Add(new Frame {
Expand All @@ -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);
}
}
}
6 changes: 6 additions & 0 deletions crashlytics/src/Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down
40 changes: 38 additions & 2 deletions crashlytics/src/cpp/android/crashlytics_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,25 @@ 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;", \
util::kFieldTypeInstance)

// clang-format on
METHOD_LOOKUP_DECLARATION(crashlytics_core,
METHOD_LOOKUP_NONE, CRASHLYTICS_CORE_FIELDS)
CRASHLYTICS_CORE_METHODS,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what the reason of making this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logFatalException method is an internal api of CrashlyticsCore, not a public api of Crashlytics. So we added it here so we can call it via jni.

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) \
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);

Expand All @@ -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;

Expand All @@ -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) &&
Expand Down Expand Up @@ -340,6 +353,29 @@ void CrashlyticsInternal::LogException(
env->DeleteLocalRef(exception_object);
}

void CrashlyticsInternal::LogExceptionAsFatal(
const char* name, const char* reason,
std::vector<firebase::crashlytics::Frame> 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);
Expand Down
5 changes: 5 additions & 0 deletions crashlytics/src/cpp/android/crashlytics_android.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class CrashlyticsInternal {
void SetUserId(const char* id);
void LogException(const char* name, const char* reason,
std::vector<firebase::crashlytics::Frame> frames);
void LogExceptionAsFatal(const char* name, const char* reason,
std::vector<firebase::crashlytics::Frame> frames);
bool IsCrashlyticsCollectionEnabled();
void SetCrashlyticsCollectionEnabled(bool enabled);

Expand Down Expand Up @@ -72,6 +74,9 @@ class CrashlyticsInternal {

// Java DataCollectionArbiter global ref.
jobject data_collection_obj_;

// Java CrashlyticsCore global ref.
jobject core_;
};

} // namespace internal
Expand Down
5 changes: 5 additions & 0 deletions crashlytics/src/cpp/common/crashlytics.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Frame> frames) {
internal_->LogExceptionAsFatal(name, reason, frames);
}

bool Crashlytics::IsCrashlyticsCollectionEnabled() {
return internal_->IsCrashlyticsCollectionEnabled();
}
Expand Down
2 changes: 2 additions & 0 deletions crashlytics/src/cpp/include/firebase/crashlytics.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class Crashlytics {
void SetUserId(const char* id);
void LogException(const char* name, const char* reason,
std::vector<Frame> frames);
void LogExceptionAsFatal(const char* name, const char* reason,
std::vector<Frame> frames);
bool IsCrashlyticsCollectionEnabled();
void SetCrashlyticsCollectionEnabled(bool enabled);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
@@ -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 <FIRCrashlytics.h>

@interface FIRExceptionModel (Platform)

@property(nonatomic) BOOL isFatal;
@property(nonatomic) BOOL onDemand;

@end
2 changes: 1 addition & 1 deletion crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 9 additions & 1 deletion crashlytics/src/cpp/ios/CrashlyticsiOSWrapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<FIRStackFrame *> *framesArray = [NSMutableArray arrayWithCapacity:frameCount];
Expand All @@ -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];
}

Expand Down
Loading