Repository navigation
Linking issue fix proposal #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wellingtoncosta
wants to merge
15
commits into
main
Choose a base branch
from
wp/linking-issue
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,289
−1,091
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
fe73068
Shared sink registry
wellingtoncosta c3b961d
Add CrashKiOS Swift package
wellingtoncosta 4afb473
Rewrite bugsnag module
wellingtoncosta 108f563
Rewrite crashlytics module
wellingtoncosta 53fa772
Remove linker-flag gradle plugins
wellingtoncosta bce9d2e
Update samples
wellingtoncosta 10b626a
Implement a single API for configuration
wellingtoncosta 6867d2e
Drop remaining references to the removed linker plugins
wellingtoncosta 95d21af
Address PR review feedback
wellingtoncosta 419d33f
Substitute :core in the sample composite builds
wellingtoncosta 3247308
PR feedback
wellingtoncosta ba3cd28
Update docs
wellingtoncosta 526e929
Enforce explicit API
wellingtoncosta 1d47a15
Add more tests
wellingtoncosta 4b2d779
Update editorconfig
wellingtoncosta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,3 +71,6 @@ fastlane/test_output | |
|
|
||
| buildboth.sh | ||
| Pods | ||
| .build/ | ||
| Package.resolved | ||
| .swiftpm/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // swift-tools-version: 5.5 | ||
|
|
||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "CrashKiOS", | ||
| platforms: [.iOS(.v15), .macOS(.v10_15), .tvOS(.v15), .watchOS(.v7)], | ||
| products: [ | ||
| .library( | ||
| name: "CrashKiOSCrashlytics", | ||
| targets: ["CrashKiOSCrashlytics"] | ||
| ), | ||
| .library( | ||
| name: "CrashKiOSBugsnag", | ||
| targets: ["CrashKiOSBugsnag"] | ||
| ), | ||
| ], | ||
| dependencies: [ | ||
| .package( | ||
| url: "https://github.com/firebase/firebase-ios-sdk.git", | ||
| "11.0.0"..<"13.0.0" | ||
| ), | ||
| .package( | ||
| url: "https://github.com/bugsnag/bugsnag-cocoa.git", | ||
| "6.26.2"..<"7.0.0" | ||
| ), | ||
| ], | ||
| targets: [ | ||
| // Header-only targets: they own the sink protocols (cinterop'd by the Kotlin | ||
| // modules) so Swift sinks compile without importing the app's Kotlin framework. | ||
| .target( | ||
| name: "CrashKiOSCrashlyticsObjC", | ||
| path: "Sources/CrashKiOSCrashlyticsObjC" | ||
| ), | ||
| .target( | ||
| name: "CrashKiOSCrashlytics", | ||
| dependencies: [ | ||
| .target(name: "CrashKiOSCrashlyticsObjC"), | ||
| .product(name: "FirebaseCrashlytics", package: "firebase-ios-sdk"), | ||
| ], | ||
| path: "Sources/CrashKiOSCrashlytics" | ||
| ), | ||
| .target( | ||
| name: "CrashKiOSBugsnagObjC", | ||
| path: "Sources/CrashKiOSBugsnagObjC" | ||
| ), | ||
| .target( | ||
| name: "CrashKiOSBugsnag", | ||
| dependencies: [ | ||
| .target(name: "CrashKiOSBugsnagObjC"), | ||
| .product(name: "Bugsnag", package: "bugsnag-cocoa"), | ||
| ], | ||
| path: "Sources/CrashKiOSBugsnag" | ||
| ), | ||
| ] | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import Bugsnag | ||
| import CrashKiOSBugsnagObjC | ||
| import Foundation | ||
| import ObjectiveC | ||
|
|
||
| /// Feature flag that marks the Kotlin termination crash. | ||
| /// This flag is public, so custom `CrashKiOSBugsnagSink` implementations can follow the same contract. | ||
| public let kotlinCrashedFeatureFlag = "crashkios.kotlin_crashed" | ||
|
|
||
| /// Prepares `config` for Kotlin crash handling. | ||
| /// | ||
| /// This function does two things: | ||
| /// - It installs an OnSendError filter. The filter suppresses the duplicate termination crash that follows a recorded Kotlin fatal. | ||
| /// - It works around Bugsnag 6.26.2+, which refuses to persist synthetic unhandled events. | ||
| /// | ||
| /// Custom sink implementations MUST call this function before `Bugsnag.start(with: config)`. | ||
| /// If they do not, Bugsnag reports every fatal Kotlin exception twice. | ||
| public func configureBugsnagForKotlin(_ config: BugsnagConfiguration) { | ||
| overrideOriginalUnhandledValue() | ||
| config.addOnSendError { event in | ||
| // Drop Bugsnag's own report of the abort that terminates the app. | ||
| // Drop it only when a Kotlin fatal is already recorded (markFatalCrashRecorded sets the flag). | ||
| !event.unhandled || !event.featureFlags.contains { $0.name == kotlinCrashedFeatureFlag } | ||
| } | ||
| config.clearFeatureFlag(name: kotlinCrashedFeatureFlag) | ||
| } | ||
|
|
||
| /// Reference `CrashKiOSBugsnagSink` implementation. | ||
| /// | ||
| /// Start Bugsnag through this class. The class prepares the configuration for Kotlin | ||
| /// crash handling. Then configure CrashKiOS with the returned sink: | ||
| /// ```swift | ||
| /// let sink = BugsnagSink.start(BugsnagConfiguration.loadConfig()) | ||
| /// CrashKiOS.shared.configure(crashReporting: BugsnagCrashReporting(sink: sink)) | ||
| /// ``` | ||
| /// Add `export("co.touchlab.crashkios:bugsnag")` AND `export("co.touchlab.crashkios:core")` | ||
| /// to the Kotlin framework for clean names. | ||
| public final class BugsnagSink: NSObject, CrashKiOSBugsnagSink { | ||
|
|
||
| /// Prepares `config` with `configureBugsnagForKotlin(_:)`, starts Bugsnag, | ||
| /// and returns the sink. | ||
| public static func start(_ config: BugsnagConfiguration) -> BugsnagSink { | ||
| configureBugsnagForKotlin(config) | ||
| Bugsnag.start(with: config) | ||
| return BugsnagSink() | ||
| } | ||
|
|
||
| public func leaveBreadcrumb(_ message: String) { | ||
| Bugsnag.leaveBreadcrumb(withMessage: message) | ||
| } | ||
|
|
||
| public func notify(with exceptions: [NSException], handled: Bool) { | ||
| guard let exception = exceptions.first else { return } | ||
| // `Bugsnag.notify` persists unhandled events, so the caller can safely terminate afterwards. | ||
| // https://github.com/bugsnag/bugsnag-cocoa/blob/6bcd46f5f8dc06ac26537875d501f02b27d219a9/Bugsnag/Client/BugsnagClient.m#L744 | ||
| Bugsnag.notify(exception) { event in | ||
| if handled { | ||
| event.severity = .warning | ||
| } else { | ||
| event.unhandled = true | ||
| event.severity = .error | ||
| } | ||
| event.errors += exceptions.dropFirst().map(BugsnagError.init) | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| public func addMetadata(_ value: Any, key: String, section: String) { | ||
| Bugsnag.addMetadata(value, key: key, section: section) | ||
| } | ||
|
|
||
| public func markFatalCrashRecorded() { | ||
| // Only the Kotlin unhandled-exception hook calls this method, right before termination. | ||
| // A direct sendFatalException() call must never trigger it. That call would poison | ||
| // the session, and the OnSendError filter would discard later genuine crashes. | ||
| Bugsnag.addFeatureFlag(name: kotlinCrashedFeatureFlag) | ||
| } | ||
| } | ||
|
|
||
| /// In Bugsnag 6.26.2+, the `originalUnhandledValue` property prevents Bugsnag from storing our | ||
| /// synthetic unhandled exceptions to disk. This function aliases it to `unhandled`. | ||
| /// https://github.com/bugsnag/bugsnag-cocoa/pull/1549 | ||
| private func overrideOriginalUnhandledValue() { | ||
| guard let handledStateClass = NSClassFromString("BugsnagHandledState"), | ||
| let method = class_getInstanceMethod(handledStateClass, NSSelectorFromString("unhandled")) | ||
| else { | ||
| assertionFailure("CrashKiOS: BugsnagHandledState internals changed — Kotlin fatal crashes may not be persisted. Update CrashKiOSBugsnag.") | ||
| return | ||
| } | ||
| // Bugsnag < 6.26.2 has no originalUnhandledValue. Nothing to override. | ||
| guard let originalMethod = class_getInstanceMethod(handledStateClass, NSSelectorFromString("originalUnhandledValue")) | ||
| else { return } | ||
| method_setImplementation(originalMethod, method_getImplementation(method)) | ||
| } | ||
|
|
||
| private extension BugsnagError { | ||
| /// Creates a BugsnagError from a (Kotlin-synthesized) NSException. | ||
| convenience init(_ exception: NSException) { | ||
| self.init() | ||
| errorClass = exception.name.rawValue | ||
| errorMessage = exception.reason | ||
| stacktrace = BugsnagStackframe.stackframes(withCallStackReturnAddresses: exception.callStackReturnAddresses) | ||
| type = .cocoa | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| // We need this empty file, else SPM won't build this header-only target. |
42 changes: 42 additions & 0 deletions
42
Sources/CrashKiOSBugsnagObjC/include/CrashKiOSBugsnagSink.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| #ifndef CrashKiOSBugsnagSink_h | ||
| #define CrashKiOSBugsnagSink_h | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| /// Receives crash-reporting events from Kotlin and forwards them to Bugsnag. | ||
| /// | ||
| /// Implement this protocol in Swift, in the app's build graph, where Bugsnag is already | ||
| /// linked. Pass the implementation to the Kotlin side with | ||
| /// `CrashKiOS.shared.configure(crashReporting: BugsnagCrashReporting(sink:))`. | ||
| /// | ||
| /// Contract for implementations (the shipped `BugsnagSink` follows all of it): | ||
| /// - MUST NOT throw. Kotlin cannot catch ObjC exceptions thrown across this | ||
| /// boundary (KT-53004), and the fatal path runs inside the unhandled-exception hook. | ||
| /// - MUST prepare the Bugsnag configuration with `configureBugsnagForKotlin(_:)` | ||
| /// (CrashKiOSBugsnag package) before `Bugsnag.start`. If it does not, Bugsnag reports | ||
| /// every fatal Kotlin exception twice: once from the Kotlin notify and once from the | ||
| /// termination abort. | ||
| @protocol CrashKiOSBugsnagSink <NSObject> | ||
|
|
||
| /// Bugsnag `leaveBreadcrumbWithMessage:`. | ||
| - (void)leaveBreadcrumb:(NSString * _Nonnull)message; | ||
|
|
||
| /// Notifies Bugsnag of a Kotlin exception. `exceptions` is `[main, cause, causeOfCause, ...]`. | ||
| /// The `callStackReturnAddresses` of each element carry its Kotlin stack trace. | ||
| /// When `handled` is NO, the implementation MUST persist the event synchronously before it | ||
| /// returns (`Bugsnag.notify` does). The process may terminate right after. | ||
| - (void)notifyWithExceptions:(NSArray<NSException *> * _Nonnull)exceptions handled:(BOOL)handled; | ||
|
|
||
| /// Bugsnag `addMetadata:key:section:`. | ||
| - (void)addMetadata:(id _Nonnull)value key:(NSString * _Nonnull)key section:(NSString * _Nonnull)section; | ||
|
|
||
| /// Only the Kotlin unhandled-exception hook calls this method. The hook calls it right after | ||
| /// the fatal notify, just before the process terminates. The implementation flags the | ||
| /// session (Bugsnag feature flag `crashkios.kotlin_crashed`). The OnSendError filter that | ||
| /// `configureBugsnagForKotlin(_:)` installs then drops Bugsnag's own report of the | ||
| /// termination abort. A direct `sendFatalException()` call does NOT trigger this method. | ||
| - (void)markFatalCrashRecorded; | ||
|
|
||
| @end | ||
|
|
||
| #endif /* CrashKiOSBugsnagSink_h */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import Foundation | ||
| import CrashKiOSCrashlyticsObjC | ||
| import FirebaseCrashlytics | ||
|
|
||
| /// Reference `CrashKiOSCrashlyticsSink` implementation. | ||
| /// | ||
| /// Configure CrashKiOS in your `AppDelegate`, right after you configure Firebase and before | ||
| /// any Kotlin code runs: | ||
| /// ```swift | ||
| /// FirebaseApp.configure() | ||
| /// CrashKiOS.shared.configure(crashReporting: CrashlyticsCrashReporting(sink: CrashlyticsSink())) | ||
| /// ``` | ||
| /// Add `export("co.touchlab.crashkios:crashlytics")` AND `export("co.touchlab.crashkios:core")` | ||
| /// to the framework for clean names. | ||
| public final class CrashlyticsSink: NSObject, CrashKiOSCrashlyticsSink { | ||
|
|
||
| private let crashlytics: Crashlytics | ||
|
|
||
| public init(_ crashlytics: Crashlytics = Crashlytics.crashlytics()) { | ||
| self.crashlytics = crashlytics | ||
| } | ||
|
|
||
| public func logMessage(_ message: String) { | ||
| crashlytics.log(message) | ||
| } | ||
|
|
||
| public func recordHandledException(withName name: String, reason: String, stackAddresses: [NSNumber]) { | ||
| let model = ExceptionModel(name: name, reason: reason) | ||
| model.stackTrace = stackAddresses.map { StackFrame(address: $0.uintValue) } | ||
| crashlytics.record(exceptionModel: model) | ||
| } | ||
|
|
||
| public func recordFatalException(_ exception: NSException) { | ||
| // This call persists synchronously. Crashlytics stores a single fatal per session, | ||
| // so it does not report the termination abort that follows a second time. | ||
| FIRCLSExceptionRecordNSException(exception) | ||
| } | ||
|
|
||
| public func setCustomValue(_ value: Any?, forKey key: String) { | ||
| crashlytics.setCustomValue(value, forKey: key) | ||
| } | ||
|
Copilot marked this conversation as resolved.
Resolved
|
||
|
|
||
| public func setUserId(_ identifier: String) { | ||
| crashlytics.setUserID(identifier) | ||
| } | ||
|
|
||
| public func setCollectionEnabled(_ enabled: Bool) { | ||
| crashlytics.setCrashlyticsCollectionEnabled(enabled) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| // We need this empty file, else SPM won't build this header-only target. |
41 changes: 41 additions & 0 deletions
41
Sources/CrashKiOSCrashlyticsObjC/include/CrashKiOSCrashlyticsSink.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| #ifndef CrashKiOSCrashlyticsSink_h | ||
| #define CrashKiOSCrashlyticsSink_h | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| /// Receives crash-reporting events from Kotlin and forwards them to Firebase Crashlytics. | ||
| /// | ||
| /// Implemented in Swift, inside the app's build graph — where FirebaseCrashlytics is | ||
| /// already linked — and handed to the Kotlin side with | ||
| /// `CrashKiOS.shared.configure(crashReporting: CrashlyticsCrashReporting(sink:))`. | ||
| /// | ||
| /// Implementations MUST NOT throw: Kotlin cannot catch ObjC exceptions thrown across | ||
| /// this boundary (KT-53004), and the fatal path runs inside the unhandled-exception hook. | ||
| @protocol CrashKiOSCrashlyticsSink <NSObject> | ||
|
|
||
| /// Crashlytics `log:`. | ||
| - (void)logMessage:(NSString * _Nonnull)message; | ||
|
|
||
| /// A non-fatal exception. `addresses` are Kotlin stack-trace return addresses, | ||
| /// mappable to `FIRStackFrame stackFrameWithAddress:`. | ||
| - (void)recordHandledExceptionWithName:(NSString * _Nonnull)name | ||
| reason:(NSString * _Nonnull)reason | ||
| stackAddresses:(NSArray<NSNumber *> * _Nonnull)addresses; | ||
|
|
||
| /// A fatal exception. The process terminates right after this returns — | ||
| /// the record must be persisted synchronously (FIRCLSExceptionRecordNSException does). | ||
| /// `exception.callStackReturnAddresses` carries the Kotlin stack trace. | ||
| - (void)recordFatalException:(NSException * _Nonnull)exception; | ||
|
|
||
| /// Crashlytics `setCustomValue:forKey:`. | ||
| - (void)setCustomValue:(id _Nullable)value forKey:(NSString * _Nonnull)key; | ||
|
|
||
| /// Crashlytics `setUserID:`. | ||
| - (void)setUserId:(NSString * _Nonnull)identifier; | ||
|
|
||
| /// Crashlytics `setCrashlyticsCollectionEnabled:`. | ||
| - (void)setCollectionEnabled:(BOOL)enabled; | ||
|
|
||
| @end | ||
|
|
||
| #endif /* CrashKiOSCrashlyticsSink_h */ |
30 changes: 30 additions & 0 deletions
30
Sources/CrashKiOSCrashlyticsObjC/include/FIRCLSException.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| #ifndef CrashKiOSFIRCLSException_h | ||
| #define CrashKiOSFIRCLSException_h | ||
|
|
||
| // Declaration copied from firebase-ios-sdk's private header: | ||
| // https://github.com/firebase/firebase-ios-sdk/blob/main/Crashlytics/Crashlytics/Handlers/FIRCLSException.h | ||
| // | ||
| // Copyright 2019-2020 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. | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| // Records `exception` as the session's fatal exception and persists it synchronously. | ||
| // Private Crashlytics SPI, resolved HARD at app link time via the FirebaseCrashlytics | ||
| // product this target depends on — nothing dynamic is left for the App Store's | ||
| // deployment-processing strip pass to break (the CrashKiOS <= 0.9.0 failure mode). | ||
| // Fallback if Firebase ever removes it: -[FIRCrashlytics recordOnDemandExceptionModel:]. | ||
| extern void FIRCLSExceptionRecordNSException(NSException *exception); | ||
|
|
||
| #endif /* CrashKiOSFIRCLSException_h */ |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.