Skip to content

Commit

Permalink
Attachment support for crash reports (#59)
Browse files Browse the repository at this point in the history
Attachment support for crash reports
  • Loading branch information
rqbacktrace authored Apr 14, 2021
1 parent 6cb3b4b commit f4dd272
Show file tree
Hide file tree
Showing 22 changed files with 512 additions and 73 deletions.
2 changes: 1 addition & 1 deletion Backtrace.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Pod::Spec.new do |s|

s.name = "Backtrace"
s.version = "1.6.0"
s.version = "1.6.1"
s.summary = "Backtrace's integration with iOS, macOS and tvOS"
s.description = "Reliable crash and hang reporting for iOS, macOS and tvOS."
s.homepage = "https://backtrace.io/"
Expand Down
86 changes: 72 additions & 14 deletions Backtrace.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Examples/Example-iOS-ObjC/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
backtraceDatabaseSettings.retryInterval = 5;
backtraceDatabaseSettings.retryLimit = 3;
backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval;
backtraceDatabaseSettings.retryOrder = RetryOderStack;
backtraceDatabaseSettings.retryOrder = RetryOrderStack;

BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc]
initWithCredentials: credentials
dbSettings: backtraceDatabaseSettings
reportsPerMin: 3
allowsAttachingDebugger: TRUE];
allowsAttachingDebugger: TRUE
detectOOM: FALSE];
BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil];
BacktraceClient.shared.delegate = self;

Expand Down
29 changes: 28 additions & 1 deletion Examples/Example-iOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!,
token: Keys.backtraceToken as String)

let backtraceDatabaseSettings = BacktraceDatabaseSettings()
backtraceDatabaseSettings.maxRecordCount = 1000
backtraceDatabaseSettings.maxDatabaseSize = 10
backtraceDatabaseSettings.retryInterval = 5
backtraceDatabaseSettings.retryLimit = 3
backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval
backtraceDatabaseSettings.retryOrder = RetryOder.queue
backtraceDatabaseSettings.retryOrder = RetryOrder.queue
let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials,
dbSettings: backtraceDatabaseSettings,
reportsPerMin: 10,
Expand All @@ -34,6 +35,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
BacktraceClient.shared?.delegate = self
BacktraceClient.shared?.attributes = ["foo": "bar", "testing": true]

let fileName = "sample.txt"
let fileUrl = try? createAndWriteFile(fileName)
var crashAttachments = Attachments()
crashAttachments[fileName] = fileUrl
BacktraceClient.shared?.attachments = crashAttachments

BacktraceClient.shared?.loggingDestinations = [BacktraceBaseDestination(level: .debug)]
do {
try throwingFunc()
Expand All @@ -46,6 +53,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {

return true
}

func createAndWriteFile(_ fileName: String) throws -> URL {
let dirName = "directory"
guard let libraryDirectoryUrl = try? FileManager.default.url(
for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
throw CustomError.runtimeError
}
let directoryUrl = libraryDirectoryUrl.appendingPathComponent(dirName)
try? FileManager().createDirectory(
at: directoryUrl,
withIntermediateDirectories: false,
attributes: nil
)
let fileUrl = directoryUrl.appendingPathComponent(fileName)
let formatter = DateFormatter()
formatter.timeStyle = .medium
let myData = formatter.string(from: Date())
try myData.write(to: fileUrl, atomically: true, encoding: .utf8)
return fileUrl
}
}

extension AppDelegate: BacktraceClientDelegate {
Expand Down
7 changes: 4 additions & 3 deletions Examples/Example-macOS-ObjC/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
backtraceDatabaseSettings.retryInterval = 5;
backtraceDatabaseSettings.retryLimit = 3;
backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval;
backtraceDatabaseSettings.retryOrder = RetryOderStack;

backtraceDatabaseSettings.retryOrder = RetryOrderStack;
BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc]
initWithCredentials: credentials
dbSettings: backtraceDatabaseSettings
reportsPerMin: 3
allowsAttachingDebugger: TRUE];
allowsAttachingDebugger: TRUE
detectOOM: FALSE];
BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil];
[BacktraceClient.shared setAttributes: @{@"foo": @"bar"}];
BacktraceClient.shared.delegate = self;
Expand Down
2 changes: 1 addition & 1 deletion Examples/Example-tvOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
backtraceDatabaseSettings.retryInterval = 5
backtraceDatabaseSettings.retryLimit = 3
backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval
backtraceDatabaseSettings.retryOrder = RetryOder.queue
backtraceDatabaseSettings.retryOrder = RetryOrder.queue
let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials,
dbSettings: backtraceDatabaseSettings,
reportsPerMin: 10,
Expand Down
5 changes: 5 additions & 0 deletions Sources/Features/Attributes/AttributesProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class AttributesProvider {

// attributes can be modified on runtime
var attributes: Attributes = [:]
var attachments: Attachments = [:]
private let attributesSources: [AttributesSource]
private let faultInfo: FaultInfo

Expand Down Expand Up @@ -43,6 +44,10 @@ extension AttributesProvider: SignalContext {
self.attributes["error.type"] = errorType
}

var attachmentPaths: [String] {
return attachments.map(\.value.path)
}

var allAttributes: Attributes {
return attributes + defaultAttributes
}
Expand Down
66 changes: 23 additions & 43 deletions Sources/Features/Attributes/AttributesStorage.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

final class AttributesStorage {
struct Config {
enum AttributesStorage {
struct AttributesConfig: Config {
let cacheUrl: URL
let directoryUrl: URL
let fileUrl: URL
Expand All @@ -17,58 +17,38 @@ final class AttributesStorage {
}
}

private static let directoryName = Bundle(for: AttributesStorage.self).bundleIdentifier ?? "BacktraceCache"
private static let directoryName = Bundle.main.bundleIdentifier ?? "BacktraceCache"

static func store(_ attributes: Attributes, fileName: String) throws {
let config = try Config(fileName: fileName)

if !FileManager.default.fileExists(atPath: config.directoryUrl.path) {
try FileManager.default.createDirectory(atPath: config.directoryUrl.path,
withIntermediateDirectories: false,
attributes: nil)
}

if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) {
try (attributes as NSDictionary).write(to: config.fileUrl)
} else {
guard (attributes as NSDictionary).write(to: config.fileUrl, atomically: true) else {
throw FileError.fileNotWritten
}
}
try store(attributes, fileName: fileName, storage: ReportMetadataStorageImpl.self)
}

static func store<T: ReportMetadataStorage>(_ attributes: Attributes, fileName: String, storage: T.Type) throws {
let config = try AttributesConfig(fileName: fileName)
try T.storeToFile(attributes, config: config)
BacktraceLogger.debug("Stored attributes at path: \(config.fileUrl)")
}

static func retrieve(fileName: String) throws -> Attributes {
let config = try Config(fileName: fileName)
guard FileManager.default.fileExists(atPath: config.fileUrl.path) else {
throw FileError.fileNotExists
}
// load file to NSDictionary
let dictionary: NSDictionary
if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) {
dictionary = try NSDictionary(contentsOf: config.fileUrl, error: ())
} else {
guard let dictionaryFromFile = NSDictionary(contentsOf: config.fileUrl) else {
throw FileError.invalidPropertyList
}
dictionary = dictionaryFromFile
}
// cast safety to AttributesType
guard let attributes: Attributes = dictionary as? Attributes else {
throw FileError.invalidPropertyList
}
try retrieve(fileName: fileName, storage: ReportMetadataStorageImpl.self)
}

static func retrieve<T: ReportMetadataStorage>(fileName: String, storage: T.Type) throws -> Attributes {
let config = try AttributesConfig(fileName: fileName)
let dictionary = try T.retrieveFromFile(config: config)
// cast safely to AttributesType
let attributes: Attributes = dictionary as Attributes
BacktraceLogger.debug("Retrieved attributes from path: \(config.fileUrl)")
return attributes
}

static func remove(fileName: String) throws {
let config = try Config(fileName: fileName)
// check file exists
guard FileManager.default.fileExists(atPath: config.fileUrl.path) else {
throw FileError.fileNotExists
}
// remove file
try FileManager.default.removeItem(at: config.fileUrl)
try remove(fileName: fileName, storage: ReportMetadataStorageImpl.self)
}

static func remove<T: ReportMetadataStorage>(fileName: String, storage: T.Type) throws {
let config = try AttributesConfig(fileName: fileName)
try T.removeFile(config: config)
BacktraceLogger.debug("Removed attributes at path: \(config.fileUrl)")
}
}
12 changes: 10 additions & 2 deletions Sources/Features/Client/BacktraceReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ extension BacktraceReporter: BacktraceClientCustomizing {
attributesProvider.attributes = newValue
}
}

var attachments: Attachments {
get {
return attributesProvider.attachments
} set {
attributesProvider.attachments = newValue
}
}
}

extension BacktraceReporter {
Expand All @@ -96,7 +104,7 @@ extension BacktraceReporter {
attributesProvider.set(faultMessage: faultMessage)
let resource = try reporter.generateLiveReport(exception: exception,
attributes: attributesProvider.allAttributes,
attachmentPaths: attachmentPaths)
attachmentPaths: attachmentPaths + attributesProvider.attachmentPaths)
return send(resource: resource)
}

Expand All @@ -106,7 +114,7 @@ extension BacktraceReporter {
attributesProvider.set(errorType: "Exception")
let resource = try reporter.generateLiveReport(exception: exception,
attributes: attributesProvider.allAttributes,
attachmentPaths: attachmentPaths)
attachmentPaths: attachmentPaths + attributesProvider.attachmentPaths)
return resource
}
}
Expand Down
42 changes: 42 additions & 0 deletions Sources/Features/Client/Model/AttachmentBookmarkHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

protocol AttachmentBookmarkHandler {
static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks
static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments
}

enum AttachmentBookmarkHandlerImpl: AttachmentBookmarkHandler {
static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks {
var attachmentsBookmarksDict = Bookmarks()
for attachment in attachments {
do {
let bookmark = try attachment.value.bookmarkData(options: URL.BookmarkCreationOptions.minimalBookmark)
attachmentsBookmarksDict[attachment.key] = bookmark
} catch {
BacktraceLogger.error("Could not bookmark attachment file URL. Error: \(error)")
continue
}
}
return attachmentsBookmarksDict
}

static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments {
var attachments = Attachments()
for bookmark in bookmarks {
var stale = Bool(false)
guard let fileUrl = try? URL(resolvingBookmarkData: bookmark.value,
options: URL.BookmarkResolutionOptions(),
relativeTo: nil,
bookmarkDataIsStale: &stale) else {
BacktraceLogger.error("Could not resolve file URL from bookmark")
continue
}
if stale {
BacktraceLogger.error("Bookmark data is stale. This should not happen")
continue
}
attachments[bookmark.key] = fileUrl
}
return attachments
}
}
72 changes: 72 additions & 0 deletions Sources/Features/Client/Model/AttachmentsStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Foundation

enum AttachmentsStorageError: Error {
case invalidDictionary
case invalidBookmark
}

enum AttachmentsStorage {
struct AttachmentsConfig: Config {
let cacheUrl: URL
let directoryUrl: URL
let fileUrl: URL

init(fileName: String) throws {
guard let cacheDirectoryURL =
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
throw FileError.noCacheDirectory
}
self.cacheUrl = cacheDirectoryURL
self.directoryUrl = cacheDirectoryURL.appendingPathComponent(directoryName)
self.fileUrl = directoryUrl.appendingPathComponent("\(fileName)_attachments.plist")
}
}

private static let directoryName = Bundle.main.bundleIdentifier ?? "BacktraceCache"

static func store(_ attachments: Attachments, fileName: String) throws {
try store(attachments, fileName: fileName, storage: ReportMetadataStorageImpl.self,
bookmarkHandler: AttachmentBookmarkHandlerImpl.self)
}

static func store<T: ReportMetadataStorage, U: AttachmentBookmarkHandler>
(_ attachments: Attachments, fileName: String, storage: T.Type, bookmarkHandler: U.Type) throws {
let config = try AttachmentsConfig(fileName: fileName)
let attachmentBookmarks = try U.convertAttachmentUrlsToBookmarks(attachments)
try T.storeToFile(attachmentBookmarks, config: config)
BacktraceLogger.debug("Stored attachments paths at path: \(config.fileUrl)")
}

static func retrieve(fileName: String) throws -> Attachments {
try retrieve(fileName: fileName, storage: ReportMetadataStorageImpl.self,
bookmarkHandler: AttachmentBookmarkHandlerImpl.self)
}

static func retrieve<T: ReportMetadataStorage, U: AttachmentBookmarkHandler>
(fileName: String, storage: T.Type, bookmarkHandler: U.Type) throws -> Attachments {
let config = try AttachmentsConfig(fileName: fileName)
let dictionary = try T.retrieveFromFile(config: config)

guard let bookmarks = dictionary as? Bookmarks else {
BacktraceLogger.debug("Could not convert stored dictionary to Bookmarks type")
throw AttachmentsStorageError.invalidDictionary
}
guard let attachments = try? U.extractAttachmentUrls(bookmarks) else {
BacktraceLogger.debug("Could not extract attachment URLs from stored attachments Bookmarks")
throw AttachmentsStorageError.invalidBookmark
}

BacktraceLogger.debug("Retrieved attachment paths at path: \(config.fileUrl)")
return attachments
}

static func remove(fileName: String) throws {
try remove(fileName: fileName, storage: ReportMetadataStorageImpl.self)
}

static func remove<T: ReportMetadataStorage>(fileName: String, storage: T.Type) throws {
let config = try AttachmentsConfig(fileName: fileName)
try T.removeFile(config: config)
BacktraceLogger.debug("Removed attachments paths at path: \(config.fileUrl)")
}
}
9 changes: 9 additions & 0 deletions Sources/Public/BacktraceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ extension BacktraceClient: BacktraceClientCustomizing {
reporter.attributes = newValue
}
}

/// Additional file attachments which are automatically added to each report.
@objc public var attachments: Attachments {
get {
return reporter.attachments
} set {
reporter.attachments = newValue
}
}
}

// MARK: - BacktraceReporting
Expand Down
1 change: 1 addition & 0 deletions Sources/Public/BacktraceClientConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import Foundation
/// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: `30`.
/// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger
/// is attached. Default: `false`.
/// - detectOOM: if set to `true` BacktraceClient will detect when the app is out of memory. Default: `false`.
@objc public init(credentials: BacktraceCredentials,
dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings(),
reportsPerMin: Int = 30,
Expand Down
Loading

0 comments on commit f4dd272

Please sign in to comment.