diff --git a/.gitignore b/.gitignore index e4379c8..c8f95c9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ xcuserdata/ .build Package.resolved +.idea \ No newline at end of file diff --git a/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj b/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj index c38c8e5..3aec226 100644 --- a/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj +++ b/SplunkRumCrashReporting/SplunkRumCrashReporting.xcodeproj/project.pbxproj @@ -12,8 +12,11 @@ 86461EEB26972906007C6DC0 /* SplunkRumCrashReporting.h in Headers */ = {isa = PBXBuildFile; fileRef = 86461EDD26972906007C6DC0 /* SplunkRumCrashReporting.h */; settings = {ATTRIBUTES = (Public, ); }; }; 86461EF726972964007C6DC0 /* CrashReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86461EF626972964007C6DC0 /* CrashReporting.swift */; }; 86461EFC269729C0007C6DC0 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 86461EFB269729C0007C6DC0 /* CrashReporter */; }; - 86461F0526972A11007C6DC0 /* sample.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = 86461F0426972A11007C6DC0 /* sample.plcrash */; }; + 86461F0526972A11007C6DC0 /* sample_v1.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = 86461F0426972A11007C6DC0 /* sample_v1.plcrash */; }; 86D3180A271655B300B43379 /* SplunkOtel in Frameworks */ = {isa = PBXBuildFile; productRef = 86D31809271655B300B43379 /* SplunkOtel */; }; + D774545D28E38CF40056159F /* DeviceStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = D774545C28E38CF40056159F /* DeviceStats.swift */; }; + D7C64D1228E494C50086368D /* DeviceStatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C64D1128E494C50086368D /* DeviceStatsTests.swift */; }; + D7D14290293804A200CAD87E /* sample_v2.plcrash in Resources */ = {isa = PBXBuildFile; fileRef = D7D1428F293804A200CAD87E /* sample_v2.plcrash */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,7 +37,10 @@ 86461EE826972906007C6DC0 /* CrashTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashTests.swift; sourceTree = ""; }; 86461EEA26972906007C6DC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 86461EF626972964007C6DC0 /* CrashReporting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReporting.swift; sourceTree = ""; }; - 86461F0426972A11007C6DC0 /* sample.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample.plcrash; sourceTree = ""; }; + 86461F0426972A11007C6DC0 /* sample_v1.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file.plcrash; path = sample_v1.plcrash; sourceTree = ""; }; + D774545C28E38CF40056159F /* DeviceStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStats.swift; sourceTree = ""; }; + D7C64D1128E494C50086368D /* DeviceStatsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatsTests.swift; sourceTree = ""; }; + D7D1428F293804A200CAD87E /* sample_v2.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample_v2.plcrash; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -80,6 +86,7 @@ isa = PBXGroup; children = ( 86461EF626972964007C6DC0 /* CrashReporting.swift */, + D774545C28E38CF40056159F /* DeviceStats.swift */, 86461EDD26972906007C6DC0 /* SplunkRumCrashReporting.h */, 86461EDE26972906007C6DC0 /* Info.plist */, ); @@ -89,8 +96,10 @@ 86461EE726972906007C6DC0 /* SplunkRumCrashReportingTests */ = { isa = PBXGroup; children = ( - 86461F0426972A11007C6DC0 /* sample.plcrash */, + D7D1428F293804A200CAD87E /* sample_v2.plcrash */, + 86461F0426972A11007C6DC0 /* sample_v1.plcrash */, 86461EE826972906007C6DC0 /* CrashTests.swift */, + D7C64D1128E494C50086368D /* DeviceStatsTests.swift */, 86461EEA26972906007C6DC0 /* Info.plist */, ); path = SplunkRumCrashReportingTests; @@ -203,7 +212,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 86461F0526972A11007C6DC0 /* sample.plcrash in Resources */, + 86461F0526972A11007C6DC0 /* sample_v1.plcrash in Resources */, + D7D14290293804A200CAD87E /* sample_v2.plcrash in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -215,6 +225,7 @@ buildActionMask = 2147483647; files = ( 86461EF726972964007C6DC0 /* CrashReporting.swift in Sources */, + D774545D28E38CF40056159F /* DeviceStats.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -223,6 +234,7 @@ buildActionMask = 2147483647; files = ( 86461EE926972906007C6DC0 /* CrashTests.swift in Sources */, + D7C64D1228E494C50086368D /* DeviceStatsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift b/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift index 92e13c4..56ee2fe 100644 --- a/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift +++ b/SplunkRumCrashReporting/SplunkRumCrashReporting/CrashReporting.swift @@ -20,9 +20,10 @@ import CrashReporter import SplunkOtel import OpenTelemetryApi -let CrashReportingVersionString = "0.2.0" +let CrashReportingVersionString = "0.3.0" var TheCrashReporter: PLCrashReporter? +private var customDataDictionary: [String: String] = [String: String]() func initializeCrashReporting() { let startupSpan = buildTracer().spanBuilder(spanName: "SplunkRumCrashReporting").startSpan() @@ -46,6 +47,8 @@ func initializeCrashReporting() { } TheCrashReporter = crashReporter updateCrashReportSessionId() + updateDeviceStats() + startPollingForDeviceStats() SplunkRum.addSessionIdChangeCallback { updateCrashReportSessionId() } @@ -71,7 +74,40 @@ private func buildTracer() -> Tracer { } func updateCrashReportSessionId() { - TheCrashReporter?.customData = SplunkRum.getSessionId().data(using: .utf8) + do { + customDataDictionary["sessionId"] = SplunkRum.getSessionId() + let customData = try NSKeyedArchiver.archivedData(withRootObject: customDataDictionary, requiringSecureCoding: false) + TheCrashReporter?.customData = customData + } catch { + // We have failed to archive the custom data dictionary. + SplunkRum.debugLog("Failed to add the sessionId to the crash reports custom data.") + } +} + +private func updateDeviceStats() { + do { + customDataDictionary["batteryLevel"] = DeviceStats.batteryLevel + customDataDictionary["freeDiskSpace"] = DeviceStats.freeDiskSpace + customDataDictionary["freeMemory"] = DeviceStats.freeMemory + let customData = try NSKeyedArchiver.archivedData(withRootObject: customDataDictionary, requiringSecureCoding: false) + TheCrashReporter?.customData = customData + } catch { + // We have failed to archive the custom data dictionary. + SplunkRum.debugLog("Failed to add the device stats to the crash reports custom data.") + } +} + +/* + Will poll every 5 seconds to update the device stats. + */ +private func startPollingForDeviceStats() { + let repeatSeconds: Double = 5 + DispatchQueue.global(qos: .background).async { + let timer = Timer.scheduledTimer(withTimeInterval: repeatSeconds, repeats: true) { _ in + updateDeviceStats() + } + timer.fire() + } } func loadPendingCrashReport(_ data: Data!) throws { @@ -86,7 +122,15 @@ func loadPendingCrashReport(_ data: Data!) throws { let span = buildTracer().spanBuilder(spanName: exceptionType ?? "unknown").setStartTime(time: now).setNoParent().startSpan() span.setAttribute(key: "component", value: "crash") if report.customData != nil { - span.setAttribute(key: "crash.rumSessionId", value: String(decoding: report.customData, as: UTF8.self)) + let customData = NSKeyedUnarchiver.unarchiveObject(with: report.customData) as? [String: String] + if customData != nil { + span.setAttribute(key: "crash.rumSessionId", value: customData!["sessionId"]!) + span.setAttribute(key: "crash.batteryLevel", value: customData!["batteryLevel"]!) + span.setAttribute(key: "crash.freeDiskSpace", value: customData!["freeDiskSpace"]!) + span.setAttribute(key: "crash.freeMemory", value: customData!["freeMemory"]!) + } else { + span.setAttribute(key: "crash.rumSessionId", value: String(decoding: report.customData, as: UTF8.self)) + } } // "marketing version" here matches up to our use of CFBundleShortVersionString span.setAttribute(key: "crash.app.version", value: report.applicationInfo.applicationMarketingVersion) @@ -105,7 +149,7 @@ func loadPendingCrashReport(_ data: Data!) throws { span.end(time: now) } -// FIXME this is a messy copy+paste of select bits of PLCrashReportTextForamtter +// FIXME this is a messy copy+paste of select bits of PLCrashReportTextFormatter func crashedThreadToStack(report: PLCrashReport, thread: PLCrashReportThreadInfo) -> String { let text = NSMutableString() text.appendFormat("Thread %ld", thread.threadNumber) diff --git a/SplunkRumCrashReporting/SplunkRumCrashReporting/DeviceStats.swift b/SplunkRumCrashReporting/SplunkRumCrashReporting/DeviceStats.swift new file mode 100644 index 0000000..06fe0b7 --- /dev/null +++ b/SplunkRumCrashReporting/SplunkRumCrashReporting/DeviceStats.swift @@ -0,0 +1,65 @@ +// +/* +Copyright 2021 Splunk Inc. + +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 +import System +import UIKit + +internal class DeviceStats { + class var batteryLevel: String { + UIDevice.current.isBatteryMonitoringEnabled = true + let level = abs(UIDevice.current.batteryLevel * 100) + return "\(level)%" + } + class var freeDiskSpace: String { + do { + let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) + let maybeFreeSpace = (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value + guard let freeSpace = maybeFreeSpace else { + return "Unknown" + } + return ByteCountFormatter.string(fromByteCount: freeSpace, countStyle: .file) + } catch { + return "Unknown" + } + } + // https://stackoverflow.com/questions/5012886/determining-the-available-amount-of-ram-on-an-ios-device/8540665#8540665 + class var freeMemory: String { + var usedBytes: Float = 0 + let totalBytes = Float(ProcessInfo.processInfo.physicalMemory) + var info = mach_task_basic_info() + var count = mach_msg_type_number_t(MemoryLayout.size) / 4 + let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { + $0.withMemoryRebound(to: integer_t.self, capacity: 1) { + task_info( + mach_task_self_, + task_flavor_t(MACH_TASK_BASIC_INFO), + $0, + &count + ) + } + } + if kerr == KERN_SUCCESS { + usedBytes = Float(info.resident_size) + } else { + return "Unknown" + } + let freeBytes = totalBytes - usedBytes + return ByteCountFormatter.string(fromByteCount: Int64(freeBytes), countStyle: .memory) + } + +} diff --git a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift index f736cd1..144f861 100644 --- a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift +++ b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/CrashTests.swift @@ -43,8 +43,8 @@ class TestSpanExporter: SpanExporter { } class CrashTests: XCTestCase { - func testBasics() throws { - let crashPath = Bundle(for: CrashTests.self).url(forResource: "sample", withExtension: "plcrash")! + func testBasics_v1() throws { + let crashPath = Bundle(for: CrashTests.self).url(forResource: "sample_v1", withExtension: "plcrash")! let crashData = try Data(contentsOf: crashPath) SplunkRum.initialize(beaconUrl: "http://127.0.0.1:8989/v1/traces", rumAuth: "FAKE", options: SplunkRumOptions(allowInsecureBeacon: true, debug: true)) @@ -75,4 +75,39 @@ class CrashTests: XCTestCase { XCTAssertEqual(startup!.attributes["component"]?.description, "appstart") } + func testBasics_v2() throws { + let crashPath = Bundle(for: CrashTests.self).url(forResource: "sample_v2", withExtension: "plcrash")! + let crashData = try Data(contentsOf: crashPath) + + SplunkRum.initialize(beaconUrl: "http://127.0.0.1:8989/v1/traces", rumAuth: "FAKE", options: SplunkRumOptions(allowInsecureBeacon: true, debug: true)) + OpenTelemetrySDK.instance.tracerProvider.addSpanProcessor(SimpleSpanProcessor(spanExporter: TestSpanExporter())) + localSpans.removeAll() + + SplunkRumCrashReporting.start() + try loadPendingCrashReport(crashData) + + XCTAssertEqual(localSpans.count, 4) + let crashReport = localSpans.first(where: { (span) -> Bool in + return span.name == "SIGTRAP" + }) + let startup = localSpans.first(where: { (span) -> Bool in + return span.name == "SplunkRumCrashReporting" + }) + + XCTAssertNotNil(crashReport) + XCTAssertNotEqual(crashReport!.attributes["splunk.rumSessionId"], crashReport!.attributes["crash.rumSessionId"]) + XCTAssertEqual(crashReport!.attributes["crash.rumSessionId"]?.description, "388e59237de675ef8e9751fcf2b0f936") + XCTAssertEqual(crashReport!.attributes["crash.address"]?.description, "7595465412") + XCTAssertEqual(crashReport!.attributes["component"]?.description, "crash") + XCTAssertEqual(crashReport!.attributes["error"]?.description, "true") + XCTAssertEqual(crashReport!.attributes["exception.type"]?.description, "SIGTRAP") + XCTAssertTrue(crashReport!.attributes["exception.stacktrace"]?.description.contains("UIKitCore") ?? false) + XCTAssertEqual(crashReport!.attributes["crash.batteryLevel"]?.description, "91.0%") + XCTAssertEqual(crashReport!.attributes["crash.freeDiskSpace"]?.description, "197.23 GB") + XCTAssertEqual(crashReport!.attributes["crash.freeMemory"]?.description, "5.54 GB") + + XCTAssertNotNil(startup) + XCTAssertEqual(startup!.attributes["component"]?.description, "appstart") + + } } diff --git a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/DeviceStatsTests.swift b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/DeviceStatsTests.swift new file mode 100644 index 0000000..b7c1a1c --- /dev/null +++ b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/DeviceStatsTests.swift @@ -0,0 +1,37 @@ +// +/* +Copyright 2021 Splunk Inc. + +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. +*/ + +@testable import SplunkRumCrashReporting +import Foundation +import XCTest + +class DeviceStatsTests: XCTestCase { + func testBattery() throws { + let batteryLevel = DeviceStats.batteryLevel + XCTAssertEqual(batteryLevel, "100.0%") + } + func testFreeDiskSpace() throws { + let diskSpace = DeviceStats.freeDiskSpace + let space = Int(diskSpace.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? 0 + XCTAssertTrue(space > 0) + } + func testFreeMemory() throws { + let freeMemory = DeviceStats.freeMemory + let space = Int(freeMemory.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? 0 + XCTAssertTrue(space > 0) + } +} diff --git a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample.plcrash b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v1.plcrash similarity index 100% rename from SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample.plcrash rename to SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v1.plcrash diff --git a/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v2.plcrash b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v2.plcrash new file mode 100644 index 0000000..7860010 Binary files /dev/null and b/SplunkRumCrashReporting/SplunkRumCrashReportingTests/sample_v2.plcrash differ