Skip to content
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

RUM-2925 feat: Add backtrace generation capability to DatadogCoreProtocol #1687

Merged
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth having this lint rule global to all the tests


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)
Comment on lines -232 to +233
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a miss - we need to create storage directories only for DatadogRemoteFeature. Otherwise we will be creating it for NetworkInstrumentationFeature and BacktraceReportingFeature unnecessarily. Added extra unit tests for this change.


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))
}
Comment on lines +41 to +43
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be more elegant, but would require revamping the startup sequence for DatadogCrashReporting. It still depends on CrashReportingPlugin, which was V1 dependency-inverter, no longer required in V2. This refactor is medium-size, hence I didn't want to do it for this PR.


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