Skip to content

Commit f4dd272

Browse files
authored
Attachment support for crash reports (#59)
Attachment support for crash reports
1 parent 6cb3b4b commit f4dd272

22 files changed

+512
-73
lines changed

Backtrace.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Pod::Spec.new do |s|
1010

1111
s.name = "Backtrace"
12-
s.version = "1.6.0"
12+
s.version = "1.6.1"
1313
s.summary = "Backtrace's integration with iOS, macOS and tvOS"
1414
s.description = "Reliable crash and hang reporting for iOS, macOS and tvOS."
1515
s.homepage = "https://backtrace.io/"

Backtrace.xcodeproj/project.pbxproj

Lines changed: 72 additions & 14 deletions
Large diffs are not rendered by default.

Examples/Example-iOS-ObjC/AppDelegate.m

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
1919
backtraceDatabaseSettings.retryInterval = 5;
2020
backtraceDatabaseSettings.retryLimit = 3;
2121
backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval;
22-
backtraceDatabaseSettings.retryOrder = RetryOderStack;
22+
backtraceDatabaseSettings.retryOrder = RetryOrderStack;
2323

2424
BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc]
2525
initWithCredentials: credentials
2626
dbSettings: backtraceDatabaseSettings
2727
reportsPerMin: 3
28-
allowsAttachingDebugger: TRUE];
28+
allowsAttachingDebugger: TRUE
29+
detectOOM: FALSE];
2930
BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil];
3031
BacktraceClient.shared.delegate = self;
3132

Examples/Example-iOS/AppDelegate.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
1818
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
1919
let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: Keys.backtraceUrl as String)!,
2020
token: Keys.backtraceToken as String)
21+
2122
let backtraceDatabaseSettings = BacktraceDatabaseSettings()
2223
backtraceDatabaseSettings.maxRecordCount = 1000
2324
backtraceDatabaseSettings.maxDatabaseSize = 10
2425
backtraceDatabaseSettings.retryInterval = 5
2526
backtraceDatabaseSettings.retryLimit = 3
2627
backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval
27-
backtraceDatabaseSettings.retryOrder = RetryOder.queue
28+
backtraceDatabaseSettings.retryOrder = RetryOrder.queue
2829
let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials,
2930
dbSettings: backtraceDatabaseSettings,
3031
reportsPerMin: 10,
@@ -34,6 +35,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
3435
BacktraceClient.shared?.delegate = self
3536
BacktraceClient.shared?.attributes = ["foo": "bar", "testing": true]
3637

38+
let fileName = "sample.txt"
39+
let fileUrl = try? createAndWriteFile(fileName)
40+
var crashAttachments = Attachments()
41+
crashAttachments[fileName] = fileUrl
42+
BacktraceClient.shared?.attachments = crashAttachments
43+
3744
BacktraceClient.shared?.loggingDestinations = [BacktraceBaseDestination(level: .debug)]
3845
do {
3946
try throwingFunc()
@@ -46,6 +53,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
4653

4754
return true
4855
}
56+
57+
func createAndWriteFile(_ fileName: String) throws -> URL {
58+
let dirName = "directory"
59+
guard let libraryDirectoryUrl = try? FileManager.default.url(
60+
for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
61+
throw CustomError.runtimeError
62+
}
63+
let directoryUrl = libraryDirectoryUrl.appendingPathComponent(dirName)
64+
try? FileManager().createDirectory(
65+
at: directoryUrl,
66+
withIntermediateDirectories: false,
67+
attributes: nil
68+
)
69+
let fileUrl = directoryUrl.appendingPathComponent(fileName)
70+
let formatter = DateFormatter()
71+
formatter.timeStyle = .medium
72+
let myData = formatter.string(from: Date())
73+
try myData.write(to: fileUrl, atomically: true, encoding: .utf8)
74+
return fileUrl
75+
}
4976
}
5077

5178
extension AppDelegate: BacktraceClientDelegate {

Examples/Example-macOS-ObjC/AppDelegate.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
1818
backtraceDatabaseSettings.retryInterval = 5;
1919
backtraceDatabaseSettings.retryLimit = 3;
2020
backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval;
21-
backtraceDatabaseSettings.retryOrder = RetryOderStack;
22-
21+
backtraceDatabaseSettings.retryOrder = RetryOrderStack;
22+
2323
BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc]
2424
initWithCredentials: credentials
2525
dbSettings: backtraceDatabaseSettings
2626
reportsPerMin: 3
27-
allowsAttachingDebugger: TRUE];
27+
allowsAttachingDebugger: TRUE
28+
detectOOM: FALSE];
2829
BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil];
2930
[BacktraceClient.shared setAttributes: @{@"foo": @"bar"}];
3031
BacktraceClient.shared.delegate = self;

Examples/Example-tvOS/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2424
backtraceDatabaseSettings.retryInterval = 5
2525
backtraceDatabaseSettings.retryLimit = 3
2626
backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval
27-
backtraceDatabaseSettings.retryOrder = RetryOder.queue
27+
backtraceDatabaseSettings.retryOrder = RetryOrder.queue
2828
let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials,
2929
dbSettings: backtraceDatabaseSettings,
3030
reportsPerMin: 10,

Sources/Features/Attributes/AttributesProvider.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class AttributesProvider {
1414

1515
// attributes can be modified on runtime
1616
var attributes: Attributes = [:]
17+
var attachments: Attachments = [:]
1718
private let attributesSources: [AttributesSource]
1819
private let faultInfo: FaultInfo
1920

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

47+
var attachmentPaths: [String] {
48+
return attachments.map(\.value.path)
49+
}
50+
4651
var allAttributes: Attributes {
4752
return attributes + defaultAttributes
4853
}
Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

3-
final class AttributesStorage {
4-
struct Config {
3+
enum AttributesStorage {
4+
struct AttributesConfig: Config {
55
let cacheUrl: URL
66
let directoryUrl: URL
77
let fileUrl: URL
@@ -17,58 +17,38 @@ final class AttributesStorage {
1717
}
1818
}
1919

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

2222
static func store(_ attributes: Attributes, fileName: String) throws {
23-
let config = try Config(fileName: fileName)
24-
25-
if !FileManager.default.fileExists(atPath: config.directoryUrl.path) {
26-
try FileManager.default.createDirectory(atPath: config.directoryUrl.path,
27-
withIntermediateDirectories: false,
28-
attributes: nil)
29-
}
30-
31-
if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) {
32-
try (attributes as NSDictionary).write(to: config.fileUrl)
33-
} else {
34-
guard (attributes as NSDictionary).write(to: config.fileUrl, atomically: true) else {
35-
throw FileError.fileNotWritten
36-
}
37-
}
23+
try store(attributes, fileName: fileName, storage: ReportMetadataStorageImpl.self)
24+
}
25+
26+
static func store<T: ReportMetadataStorage>(_ attributes: Attributes, fileName: String, storage: T.Type) throws {
27+
let config = try AttributesConfig(fileName: fileName)
28+
try T.storeToFile(attributes, config: config)
3829
BacktraceLogger.debug("Stored attributes at path: \(config.fileUrl)")
3930
}
4031

4132
static func retrieve(fileName: String) throws -> Attributes {
42-
let config = try Config(fileName: fileName)
43-
guard FileManager.default.fileExists(atPath: config.fileUrl.path) else {
44-
throw FileError.fileNotExists
45-
}
46-
// load file to NSDictionary
47-
let dictionary: NSDictionary
48-
if #available(iOS 11.0, tvOS 11.0, macOS 10.13, *) {
49-
dictionary = try NSDictionary(contentsOf: config.fileUrl, error: ())
50-
} else {
51-
guard let dictionaryFromFile = NSDictionary(contentsOf: config.fileUrl) else {
52-
throw FileError.invalidPropertyList
53-
}
54-
dictionary = dictionaryFromFile
55-
}
56-
// cast safety to AttributesType
57-
guard let attributes: Attributes = dictionary as? Attributes else {
58-
throw FileError.invalidPropertyList
59-
}
33+
try retrieve(fileName: fileName, storage: ReportMetadataStorageImpl.self)
34+
}
35+
36+
static func retrieve<T: ReportMetadataStorage>(fileName: String, storage: T.Type) throws -> Attributes {
37+
let config = try AttributesConfig(fileName: fileName)
38+
let dictionary = try T.retrieveFromFile(config: config)
39+
// cast safely to AttributesType
40+
let attributes: Attributes = dictionary as Attributes
6041
BacktraceLogger.debug("Retrieved attributes from path: \(config.fileUrl)")
6142
return attributes
6243
}
6344

6445
static func remove(fileName: String) throws {
65-
let config = try Config(fileName: fileName)
66-
// check file exists
67-
guard FileManager.default.fileExists(atPath: config.fileUrl.path) else {
68-
throw FileError.fileNotExists
69-
}
70-
// remove file
71-
try FileManager.default.removeItem(at: config.fileUrl)
46+
try remove(fileName: fileName, storage: ReportMetadataStorageImpl.self)
47+
}
48+
49+
static func remove<T: ReportMetadataStorage>(fileName: String, storage: T.Type) throws {
50+
let config = try AttributesConfig(fileName: fileName)
51+
try T.removeFile(config: config)
7252
BacktraceLogger.debug("Removed attributes at path: \(config.fileUrl)")
7353
}
7454
}

Sources/Features/Client/BacktraceReporter.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ extension BacktraceReporter: BacktraceClientCustomizing {
7878
attributesProvider.attributes = newValue
7979
}
8080
}
81+
82+
var attachments: Attachments {
83+
get {
84+
return attributesProvider.attachments
85+
} set {
86+
attributesProvider.attachments = newValue
87+
}
88+
}
8189
}
8290

8391
extension BacktraceReporter {
@@ -96,7 +104,7 @@ extension BacktraceReporter {
96104
attributesProvider.set(faultMessage: faultMessage)
97105
let resource = try reporter.generateLiveReport(exception: exception,
98106
attributes: attributesProvider.allAttributes,
99-
attachmentPaths: attachmentPaths)
107+
attachmentPaths: attachmentPaths + attributesProvider.attachmentPaths)
100108
return send(resource: resource)
101109
}
102110

@@ -106,7 +114,7 @@ extension BacktraceReporter {
106114
attributesProvider.set(errorType: "Exception")
107115
let resource = try reporter.generateLiveReport(exception: exception,
108116
attributes: attributesProvider.allAttributes,
109-
attachmentPaths: attachmentPaths)
117+
attachmentPaths: attachmentPaths + attributesProvider.attachmentPaths)
110118
return resource
111119
}
112120
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Foundation
2+
3+
protocol AttachmentBookmarkHandler {
4+
static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks
5+
static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments
6+
}
7+
8+
enum AttachmentBookmarkHandlerImpl: AttachmentBookmarkHandler {
9+
static func convertAttachmentUrlsToBookmarks(_ attachments: Attachments) throws -> Bookmarks {
10+
var attachmentsBookmarksDict = Bookmarks()
11+
for attachment in attachments {
12+
do {
13+
let bookmark = try attachment.value.bookmarkData(options: URL.BookmarkCreationOptions.minimalBookmark)
14+
attachmentsBookmarksDict[attachment.key] = bookmark
15+
} catch {
16+
BacktraceLogger.error("Could not bookmark attachment file URL. Error: \(error)")
17+
continue
18+
}
19+
}
20+
return attachmentsBookmarksDict
21+
}
22+
23+
static func extractAttachmentUrls(_ bookmarks: Bookmarks) throws -> Attachments {
24+
var attachments = Attachments()
25+
for bookmark in bookmarks {
26+
var stale = Bool(false)
27+
guard let fileUrl = try? URL(resolvingBookmarkData: bookmark.value,
28+
options: URL.BookmarkResolutionOptions(),
29+
relativeTo: nil,
30+
bookmarkDataIsStale: &stale) else {
31+
BacktraceLogger.error("Could not resolve file URL from bookmark")
32+
continue
33+
}
34+
if stale {
35+
BacktraceLogger.error("Bookmark data is stale. This should not happen")
36+
continue
37+
}
38+
attachments[bookmark.key] = fileUrl
39+
}
40+
return attachments
41+
}
42+
}

0 commit comments

Comments
 (0)