Skip to content

Commit

Permalink
Merge pull request #1687 from DataDog/ncreated/RUM-2925/record-the-st…
Browse files Browse the repository at this point in the history
…ack-trace-of-app-hang

RUM-2925 feat: Add backtrace generation capability to `DatadogCoreProtocol`
  • Loading branch information
ncreated authored Feb 23, 2024
2 parents c85902b + 1568791 commit 91da074
Show file tree
Hide file tree
Showing 21 changed files with 616 additions and 183 deletions.
78 changes: 78 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import XCTest
import DatadogCrashReporting
@testable import DatadogInternal

/// Tests integration of `DatadogCore` and `DatadogCrashReporting` for backtrace generation.
class GeneratingBacktraceTests: XCTestCase {
private var core: DatadogCoreProxy! // swiftlint:disable:this implicitly_unwrapped_optional

override func setUp() {
super.setUp()
core = DatadogCoreProxy(context: .mockWith(trackingConsent: .granted))
}

override func tearDown() {
core.flushAndTearDown()
core = nil
super.tearDown()
}

func testGivenCrashReportingIsEnabled_thenCoreCanGenerateBacktrace() throws {
// Given
CrashReporting.enable(in: core)
XCTAssertNotNil(core.get(feature: BacktraceReportingFeature.self), "`BacktraceReportingFeature` is registered")

// When
let backtrace = try XCTUnwrap(core.backtraceReporter.generateBacktrace())

// Then
XCTAssertGreaterThan(backtrace.threads.count, 0, "Some thread(s) should be recorded")
XCTAssertGreaterThan(backtrace.binaryImages.count, 0, "Some binary image(s) should be recorded")

XCTAssertTrue(
backtrace.stack.contains("DatadogCoreTests"),
"Backtrace stack should include at least one frame from `DatadogCoreTests` image"
)
XCTAssertTrue(
backtrace.stack.contains("XCTest"),
"Backtrace stack should include at least one frame from `XCTest` image"
)
#if os(iOS)
XCTAssertTrue(
backtrace.binaryImages.contains(where: { $0.libraryName == "DatadogCoreTests iOS" }),
"Backtrace should include the image for `DatadogCoreTests iOS`"
)
#elseif os(tvOS)
XCTAssertTrue(
backtrace.binaryImages.contains(where: { $0.libraryName == "DatadogCoreTests tvOS" }),
"Backtrace should include the image for `DatadogCoreTests tvOS`"
)
#endif
XCTAssertTrue(
// Assert on prefix as it is `XCTestCore` on iOS 15+ and `XCTest` earlier:
backtrace.binaryImages.contains(where: { $0.libraryName.hasPrefix("XCTest") }),
"Backtrace should include the image for `XCTest`"
)
}
}
16 changes: 8 additions & 8 deletions DatadogCore/Sources/Core/DatadogCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,16 @@ extension DatadogCore: DatadogCoreProtocol {
///
/// - Parameter feature: The Feature instance.
func register<T>(feature: T) throws where T: DatadogFeature {
let featureDirectories = try directory.getFeatureDirectories(forFeatureNamed: T.name)
if let feature = feature as? DatadogRemoteFeature {
let featureDirectories = try directory.getFeatureDirectories(forFeatureNamed: T.name)

let performancePreset: PerformancePreset
if let override = feature.performanceOverride {
performancePreset = performance.updated(with: override)
} else {
performancePreset = performance
}
let performancePreset: PerformancePreset
if let override = feature.performanceOverride {
performancePreset = performance.updated(with: override)
} else {
performancePreset = performance
}

if let feature = feature as? DatadogRemoteFeature {
let storage = FeatureStorage(
featureName: T.name,
queue: readWriteQueue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class CrashReporterTests: XCTestCase {
feature.sendCrashReportIfFound()

waitForExpectations(timeout: 0.5, handler: nil)
XCTAssertEqual(sender.sentCrashReport, crashReport, "It should send the crash report retrieved from the `plugin`")
DDAssertReflectionEqual(sender.sentCrashReport, crashReport, "It should send the crash report retrieved from the `plugin`")
let sentCrashContext = try XCTUnwrap(sender.sentCrashContext, "It should send the crash context")
DDAssertDictionariesEqual(
try sentCrashContext.data.toJSONObject(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import XCTest
import DatadogInternal
@testable import DatadogCore

private struct RemoteFeatureMock: DatadogRemoteFeature {
static let name: String = "remote-feature-mock"

var requestBuilder: FeatureRequestBuilder = FeatureRequestBuilderMock()
var messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver()
}

private struct FeatureMock: DatadogFeature {
static let name: String = "feature-mock"

var messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver()
}

class DatadogCore_FeatureDirectoriesTests: XCTestCase {
private var core: DatadogCore! // swiftlint:disable:this implicitly_unwrapped_optional

override func setUp() {
super.setUp()
temporaryCoreDirectory.create()
core = DatadogCore(
directory: temporaryCoreDirectory,
dateProvider: SystemDateProvider(),
initialConsent: .mockRandom(),
performance: .mockRandom(),
httpClient: HTTPClientMock(),
encryption: nil,
contextProvider: .mockAny(),
applicationVersion: .mockAny(),
maxBatchesPerUpload: .mockRandom(min: 1, max: 100),
backgroundTasksEnabled: .mockAny()
)
}

override func tearDown() {
core.flushAndTearDown()
core = nil
temporaryCoreDirectory.delete()
super.tearDown()
}

func testWhenRegisteringRemoteFeature_itCreatesFeatureDirectories() throws {
// When
try core.register(feature: RemoteFeatureMock())

// Then
let featureDirectory = try temporaryCoreDirectory.coreDirectory.subdirectory(path: RemoteFeatureMock.name)
XCTAssertNoThrow(try featureDirectory.subdirectory(path: "v2"), "Authorized data directory must exist")
XCTAssertNoThrow(try featureDirectory.subdirectory(path: "intermediate-v2"), "Intermediate data directory must exist")
}

func testWhenRegisteringFeature_itDoesNotCreateFeatureDirectories() throws {
// When
try core.register(feature: FeatureMock())

// Then
XCTAssertThrowsError(
try temporaryCoreDirectory.coreDirectory.subdirectory(path: FeatureMock.name),
"Feature directory must not exist"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ internal extension DDCrashReport {
type: String = .mockAny(),
message: String = .mockAny(),
stack: String = .mockAny(),
threads: [Thread] = [],
threads: [DDThread] = [],
binaryImages: [BinaryImage] = [],
meta: Meta = .mockAny(),
wasTruncated: Bool = .mockAny(),
Expand Down
4 changes: 4 additions & 0 deletions DatadogCrashReporting/Sources/CrashReporting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public final class CrashReporting {

try core.register(feature: reporter)

if let plcr = PLCrashReporterPlugin.thirdPartyCrashReporter {
try core.register(backtraceReporter: BacktraceReporter(reporter: plcr))
}

reporter.sendCrashReportIfFound()

core.telemetry
Expand Down
165 changes: 1 addition & 164 deletions DatadogCrashReporting/Sources/CrashReportingPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,174 +5,11 @@
*/

import Foundation

/// Crash Report format supported by Datadog SDK.
@objc
internal class DDCrashReport: NSObject, Codable {
struct Thread: Codable {
/// The name of the thread, e.g. `"Thread 0"`
let name: String
/// Unsymbolicated stack trace of the crash.
let stack: String
/// If the thread was halted.
let crashed: Bool
/// Thread state (CPU registers dump), only available for halted thread.
let state: String?

init(
name: String,
stack: String,
crashed: Bool,
state: String?
) {
self.name = name
self.stack = stack
self.crashed = crashed
self.state = state
}

// MARK: - Encoding

enum CodingKeys: String, CodingKey {
case name = "name"
case stack = "stack"
case crashed = "crashed"
case state = "state"
}
}

struct BinaryImage: Codable {
let libraryName: String
let uuid: String
let architecture: String
let isSystemLibrary: Bool
let loadAddress: String
let maxAddress: String

init(
libraryName: String,
uuid: String,
architecture: String,
isSystemLibrary: Bool,
loadAddress: String,
maxAddress: String
) {
self.libraryName = libraryName
self.uuid = uuid
self.architecture = architecture
self.isSystemLibrary = isSystemLibrary
self.loadAddress = loadAddress
self.maxAddress = maxAddress
}

// MARK: - Encoding

enum CodingKeys: String, CodingKey {
case libraryName = "name"
case uuid = "uuid"
case architecture = "arch"
case isSystemLibrary = "is_system"
case loadAddress = "load_address"
case maxAddress = "max_address"
}
}

/// Meta information about the process.
/// Ref.: https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report
struct Meta: Codable {
/// A client-generated 16-byte UUID of the incident.
let incidentIdentifier: String?
/// The name of the crashed process.
let process: String?
/// Parent process information.
let parentProcess: String?
/// The location of the executable on disk.
let path: String?
/// The CPU architecture of the process that crashed.
let codeType: String?
/// The name of the corresponding BSD termination signal.
let exceptionType: String?
/// CPU specific information about the exception encoded into 64-bit hexadecimal number preceded by the signal code.
let exceptionCodes: String?

init(
incidentIdentifier: String?,
process: String?,
parentProcess: String?,
path: String?,
codeType: String?,
exceptionType: String?,
exceptionCodes: String?
) {
self.incidentIdentifier = incidentIdentifier
self.process = process
self.parentProcess = parentProcess
self.path = path
self.codeType = codeType
self.exceptionType = exceptionType
self.exceptionCodes = exceptionCodes
}

enum CodingKeys: String, CodingKey {
case incidentIdentifier = "incident_identifier"
case process = "process"
case parentProcess = "parent_process"
case path = "path"
case codeType = "code_type"
case exceptionType = "exception_type"
case exceptionCodes = "exception_codes"
}
}

/// The date of the crash occurrence.
let date: Date?
/// Crash report type - used to group similar crash reports.
/// In Datadog Error Tracking this corresponds to `error.type`.
let type: String
/// Crash report message - if possible, it should provide additional troubleshooting information in addition to the crash type.
/// In Datadog Error Tracking this corresponds to `error.message`.
let message: String
/// Unsymbolicated stack trace related to the crash (this can be either uncaugh exception backtrace or stack trace of the halted thread).
/// In Datadog Error Tracking this corresponds to `error.stack`.
let stack: String
/// All threads running in the process.
let threads: [Thread]
/// List of binary images referenced from all stack traces.
let binaryImages: [BinaryImage]
/// Meta information about the crash and process.
let meta: Meta
/// If any stack trace information was truncated due to crash report minimization.
let wasTruncated: Bool
/// The last context injected through `inject(context:)`
let context: Data?

init(
date: Date?,
type: String,
message: String,
stack: String,
threads: [Thread],
binaryImages: [BinaryImage],
meta: Meta,
wasTruncated: Bool,
context: Data?
) {
self.date = date
self.type = type
self.message = message
self.stack = stack
self.threads = threads
self.binaryImages = binaryImages
self.meta = meta
self.wasTruncated = wasTruncated
self.context = context
}
}
import DatadogInternal

/// An interface for enabling crash reporting feature in Datadog SDK.
///
/// The SDK calls each API on a background thread and succeeding calls are synchronized.
@objc
internal protocol CrashReportingPlugin: AnyObject {
/// Reads unprocessed crash report if available.
/// - Parameter completion: the completion block called with the value of `DDCrashReport` if a crash report is available
Expand Down
20 changes: 20 additions & 0 deletions DatadogCrashReporting/Sources/Integrations/BacktraceReporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-Present Datadog, Inc.
*/

import DatadogInternal

internal struct BacktraceReporter: DatadogInternal.BacktraceReporting {
let reporter: ThirdPartyCrashReporter

func generateBacktrace() -> DatadogInternal.BacktraceReport? {
do {
return try reporter.generateBacktrace()
} catch let error {
DD.logger.error("Encountered an error when generating backtrace", error: error)
return nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2019-Present Datadog, Inc.
*/

import Foundation
import DatadogInternal
import CrashReporter

/// Builds `DDCrashReport` from `PLCrashReport`.
Expand Down
Loading

0 comments on commit 91da074

Please sign in to comment.