Skip to content

Commit

Permalink
Merge pull request #1645 from DataDog/ncreated/RUM-1833/link-crash-lo…
Browse files Browse the repository at this point in the history
…gs-to-rum-session

RUM-1833 feat: Link (emergency) crash Logs to crashed RUM session
  • Loading branch information
ncreated authored Jan 25, 2024
2 parents d51149c + 3add863 commit e01e97b
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 80 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [FIX] RUM session not being linked to spans. See [#1615][]
- [FIX] `URLSessionTask.resume()` swizzling in iOS 13 and 12. See [#1637][]
- [FEATURE] Allow stopping a core instance. See [#1541][]
- [FEATURE] Link crashes sent as Log events to RUM session. See [#1645][]
- [IMPROVEMENT] Add extra HTTP codes to the list of retryable status codes. See [#1639][]
- [FEATURE] Add privacy manifest to `DatadogCore`. See [#1644][]

Expand Down Expand Up @@ -574,6 +575,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
[#1524]: https://github.com/DataDog/dd-sdk-ios/pull/1524
[#1529]: https://github.com/DataDog/dd-sdk-ios/pull/1529
[#1533]: https://github.com/DataDog/dd-sdk-ios/pull/1533
[#1645]: https://github.com/DataDog/dd-sdk-ios/pull/1645
[#1594]: https://github.com/DataDog/dd-sdk-ios/pull/1594
[#1536]: https://github.com/DataDog/dd-sdk-ios/pull/1536
[#1609]: https://github.com/DataDog/dd-sdk-ios/pull/1609
Expand Down
14 changes: 14 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@
6176C1732ABDBA2E00131A70 /* MonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176C1712ABDBA2E00131A70 /* MonitorTests.swift */; };
61776CED273BEA5500F93802 /* DebugRUMSessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61776CEC273BEA5500F93802 /* DebugRUMSessionViewController.swift */; };
61776D4E273E6D9F00F93802 /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61776D4D273E6D9F00F93802 /* SwiftUI.swift */; };
6179DB562B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */; };
6179DB572B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */; };
6179FFD3254ADB1700556A0B /* ObjcAppLaunchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */; };
6179FFDE254ADBEF00556A0B /* ObjcAppLaunchHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */; settings = {ATTRIBUTES = (Public, ); }; };
617B953D24BF4D8F00E6F443 /* RUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */; };
Expand Down Expand Up @@ -2168,6 +2170,7 @@
61776CEC273BEA5500F93802 /* DebugRUMSessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugRUMSessionViewController.swift; sourceTree = "<group>"; };
61776D4D273E6D9F00F93802 /* SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI.swift; sourceTree = "<group>"; };
61786F7624FCDE04009E6BAB /* RUMDebuggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDebuggingTests.swift; sourceTree = "<group>"; };
6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingCrashReportTests.swift; sourceTree = "<group>"; };
6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ObjcAppLaunchHandler.h; sourceTree = "<group>"; };
6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ObjcAppLaunchHandler.m; sourceTree = "<group>"; };
617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMonitorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3566,6 +3569,7 @@
610ABD492A69309900AFEA34 /* IntegrationUnitTests */ = {
isa = PBXGroup;
children = (
6179DB542B60229D00E9E04E /* CrashReporting */,
61E8C5062B28896100E709B4 /* RUM */,
610ABD4A2A6930AB00AFEA34 /* Public */,
618353BA2A6946F40085F84A /* Internal */,
Expand Down Expand Up @@ -4366,6 +4370,14 @@
path = Helpers;
sourceTree = "<group>";
};
6179DB542B60229D00E9E04E /* CrashReporting */ = {
isa = PBXGroup;
children = (
6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */,
);
path = CrashReporting;
sourceTree = "<group>";
};
617B953B24BF4D7300E6F443 /* RUMMonitor */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -7351,6 +7363,7 @@
6176991E2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift in Sources */,
617B954224BF4E7600E6F443 /* RUMMonitorConfigurationTests.swift in Sources */,
61F9CABA2513A7F5000A5E61 /* RUMSessionMatcher.swift in Sources */,
6179DB562B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */,
3C0D5DE22A543DC400446CF9 /* EventGeneratorTests.swift in Sources */,
6136CB4A2A69C29C00AC265D /* FilesOrchestrator+MetricsTests.swift in Sources */,
61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */,
Expand Down Expand Up @@ -8428,6 +8441,7 @@
D24C9C4E29A7BA41002057CF /* LogsMocks.swift in Sources */,
D2CB6EDE27C520D400A62B57 /* RUMEventMatcher.swift in Sources */,
D2CB6EE027C520D400A62B57 /* SpanMatcher.swift in Sources */,
6179DB572B6022EA00E9E04E /* SendingCrashReportTests.swift in Sources */,
61112F8F2A4417D6006FFCA6 /* DDRUM+apiTests.m in Sources */,
D2DC4BBD27F234E000E4FB96 /* CITestIntegrationTests.swift in Sources */,
D2CB6EE427C520D400A62B57 /* FeatureTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 TestUtilities
@testable import DatadogCrashReporting
import DatadogInternal
@testable import DatadogLogs
@testable import DatadogRUM

/// A crash reporter mock with two capabilities:
/// - notifying a pending crash report found at SDK init,
/// - recording crash context data injected from SDK core and features like RUM.
private class CrashReporterMock: CrashReportingPlugin {
@ReadWriteLock
internal var pendingCrashReport: DDCrashReport?
@ReadWriteLock
internal var injectedContext: Data? = nil

init(pendingCrashReport: DDCrashReport? = nil) {
self.pendingCrashReport = pendingCrashReport
}

func readPendingCrashReport(completion: (DDCrashReport?) -> Bool) { _ = completion(pendingCrashReport) }
func inject(context: Data) { injectedContext = context }
}

/// Covers broad scenarios of sending Crash Reports.
class SendingCrashReportTests: 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 testWhenSDKStartsWithPendingCrashReport_itSendsItAsLogAndRUMEvent() throws {
// Given
let crashReport: DDCrashReport = .mockRandomWith(
context: .mockWith(
trackingConsent: .granted, // CR from the app session that has enabled data collection
lastIsAppInForeground: true // CR occurred while the app was in the foreground
)
)

// When
Logs.enable(with: .init(), in: core)
RUM.enable(with: .init(applicationID: "rum-app-id"), in: core)
CrashReporting.enable(with: CrashReporterMock(pendingCrashReport: crashReport), in: core)

// Then (an emergency log is sent)
let log = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self).first)
XCTAssertEqual(log.status, .emergency)
XCTAssertEqual(log.message, crashReport.message)
XCTAssertEqual(log.error?.message, crashReport.message)
XCTAssertEqual(log.error?.kind, crashReport.type)
XCTAssertEqual(log.error?.stack, crashReport.stack)
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.threads])
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.binaryImages])
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.meta])
XCTAssertNotNil(log.attributes.internalAttributes?[DDError.wasTruncated])

// Then (RUMError is sent)
let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMCrashEvent.self).first)
XCTAssertEqual(rumEvent.model.error.message, crashReport.message)
XCTAssertEqual(rumEvent.model.error.type, crashReport.type)
XCTAssertEqual(rumEvent.model.error.stack, crashReport.stack)
XCTAssertNotNil(rumEvent.additionalAttributes?[DDError.threads])
XCTAssertNotNil(rumEvent.additionalAttributes?[DDError.binaryImages])
XCTAssertNotNil(rumEvent.additionalAttributes?[DDError.meta])
XCTAssertNotNil(rumEvent.additionalAttributes?[DDError.wasTruncated])
}

func testWhenSendingCrashReportAsLog_itIsLinkedToTheRUMSessionThatHasCrashed() throws {
let crashReporter = CrashReporterMock()

// Given (RUM session)
Logs.enable(with: .init(), in: core)
RUM.enable(with: .init(applicationID: "rum-app-id"), in: core)
CrashReporting.enable(with: crashReporter, in: core)
RUMMonitor.shared(in: core).startView(key: "view-1", name: "FirstView")

let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMViewEvent.self).last)

// Flush async tasks in Crash Reporting feature (this is yet not a part of `core.flushAndTearDown()` today)
// TODO: RUM-2766 Stop core instance with completion
(core.get(feature: CrashReportingFeature.self)!.crashContextProvider as! CrashContextCoreProvider).flush()
core.flushAndTearDown()

// When (starting an SDK with pending crash report)
core = DatadogCoreProxy()

let crashReport: DDCrashReport = .mockRandomWith( // mock a CR with context injected from previous instance of the SDK
contextData: crashReporter.injectedContext!
)

Logs.enable(with: .init(), in: core)
RUM.enable(with: .init(applicationID: "rum-app-id"), in: core)
CrashReporting.enable(with: CrashReporterMock(pendingCrashReport: crashReport), in: core)

// Then (an emergency log is sent)
let log = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: LogsFeature.name, ofType: LogEvent.self).first)
XCTAssertEqual(log.status, .emergency)
XCTAssertEqual(log.message, crashReport.message)
XCTAssertEqual(log.attributes.internalAttributes?["application_id"] as? String, rumEvent.application.id)
XCTAssertEqual(log.attributes.internalAttributes?["session_id"] as? String, rumEvent.session.id)
XCTAssertEqual(log.attributes.internalAttributes?["view.id"] as? String, rumEvent.view.id)
}
}
146 changes: 98 additions & 48 deletions DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,60 +60,57 @@ class CrashLogReceiverTests: XCTestCase {

// MARK: - Testing Uploaded Data

func testWhenSendingCrashReport_itIncludesAllErrorInformation() throws {
let dateCorrectionOffset: TimeInterval = .mockRandom()

// Given
let crashReport: DDCrashReport = .mockWith(
date: .mockDecember15th2019At10AMUTC(),
type: .mockRandom(),
message: .mockRandom(),
stack: .mockRandom(),
threads: [
.init(name: "Thread 0", stack: "thread 0 stack", crashed: true, state: nil),
.init(name: "Thread 1", stack: "thread 1 stack", crashed: false, state: nil),
.init(name: "Thread 2", stack: "thread 2 stack", crashed: false, state: nil),
],
binaryImages: [
.init(libraryName: "library1", uuid: "uuid1", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad1", maxAddress: "0xMax1"),
.init(libraryName: "library2", uuid: "uuid2", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad2", maxAddress: "0xMax2"),
.init(libraryName: "library3", uuid: "uuid3", architecture: "arch", isSystemLibrary: false, loadAddress: "0xLoad3", maxAddress: "0xMax3"),
],
meta: .init(
incidentIdentifier: "incident-identifier",
process: "process [1]",
parentProcess: "parent-process [0]",
path: "process/path",
codeType: "arch",
exceptionType: "EXCEPTION_TYPE",
exceptionCodes: "EXCEPTION_CODES"
),
wasTruncated: false
)

let mockArchitecture = String.mockRandom()
let mockOSName: String = .mockRandom()
let mockOSVersion: String = .mockRandom()
let mockOSBuild: String = .mockRandom()

let crashContext: CrashContext = .mockWith(
serverTimeOffset: dateCorrectionOffset,
private let crashReport: DDCrashReport = .mockWith(
date: .mockDecember15th2019At10AMUTC(),
type: .mockRandom(),
message: .mockRandom(),
stack: .mockRandom(),
threads: [
.init(name: "Thread 0", stack: "thread 0 stack", crashed: true, state: nil),
.init(name: "Thread 1", stack: "thread 1 stack", crashed: false, state: nil),
.init(name: "Thread 2", stack: "thread 2 stack", crashed: false, state: nil),
],
binaryImages: [
.init(libraryName: "library1", uuid: "uuid1", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad1", maxAddress: "0xMax1"),
.init(libraryName: "library2", uuid: "uuid2", architecture: "arch", isSystemLibrary: true, loadAddress: "0xLoad2", maxAddress: "0xMax2"),
.init(libraryName: "library3", uuid: "uuid3", architecture: "arch", isSystemLibrary: false, loadAddress: "0xLoad3", maxAddress: "0xMax3"),
],
meta: .init(
incidentIdentifier: "incident-identifier",
process: "process [1]",
parentProcess: "parent-process [0]",
path: "process/path",
codeType: "arch",
exceptionType: "EXCEPTION_TYPE",
exceptionCodes: "EXCEPTION_CODES"
),
wasTruncated: false
)

private func crashContextWith(lastRUMViewEvent: AnyCodable?) -> CrashContext {
return .mockWith(
serverTimeOffset: .mockRandom(),
service: .mockRandom(),
env: .mockRandom(),
version: .mockRandom(),
buildNumber: .mockRandom(),
device: .mockWith(
osName: mockOSName,
osVersion: mockOSVersion,
osBuildNumber: mockOSBuild,
architecture: mockArchitecture
osName: .mockRandom(),
osVersion: .mockRandom(),
osBuildNumber: .mockRandom(),
architecture: .mockRandom()
),
sdkVersion: .mockRandom(),
userInfo: Bool.random() ? .mockRandom() : .empty,
networkConnectionInfo: .mockRandom(),
carrierInfo: .mockRandom(),
lastRUMViewEvent: AnyCodable(mockRandomAttributes())
lastRUMViewEvent: lastRUMViewEvent
)
}

func testWhenSendingCrashReport_itEncodesErrorInformation() throws {
// Given (CR with no link to RUM view)
let crashContext = crashContextWith(lastRUMViewEvent: nil) // no RUM view information

// When
let core = PassthroughCoreMock(
Expand All @@ -130,7 +127,7 @@ class CrashLogReceiverTests: XCTestCase {
let user = try XCTUnwrap(crashContext.userInfo)

let expectedLog = LogEvent(
date: crashReport.date!.addingTimeInterval(dateCorrectionOffset),
date: crashReport.date!.addingTimeInterval(crashContext.serverTimeOffset),
status: .emergency,
message: crashReport.message,
error: .init(
Expand All @@ -146,11 +143,11 @@ class CrashLogReceiverTests: XCTestCase {
applicationVersion: crashContext.version,
applicationBuildNumber: crashContext.buildNumber,
buildId: nil,
dd: .init(device: .init(architecture: mockArchitecture)),
dd: .init(device: .init(architecture: crashContext.device.architecture)),
os: .init(
name: mockOSName,
version: mockOSVersion,
build: mockOSBuild
name: crashContext.device.osName,
version: crashContext.device.osVersion,
build: crashContext.device.osBuildNumber
),
userInfo: .init(
id: user.id,
Expand All @@ -174,4 +171,57 @@ class CrashLogReceiverTests: XCTestCase {

DDAssertJSONEqual(expectedLog, log)
}

// swiftlint:disable multiline_literal_brackets
func testWhenSendingCrashReportWithRUMContext_itEncodesErrorInformation() throws {
// Given (CR with the link to RUM view)
let crashContext = crashContextWith(
lastRUMViewEvent: AnyCodable([ // partial RUM view information, necessary for the link
"application": ["id": "rum-app-id"],
"session": ["id": "rum-session-id"],
"view": ["id": "rum-view-id"],
])
)

// When
let core = PassthroughCoreMock(
messageReceiver: CrashLogReceiver(dateProvider: SystemDateProvider())
)

let sender = MessageBusSender(core: core)
sender.send(report: crashReport, with: crashContext)

// Then
let log = try XCTUnwrap(core.events(ofType: LogEvent.self).first)

XCTAssertEqual(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.applicationID] as? String, "rum-app-id")
XCTAssertEqual(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.sessionID] as? String, "rum-session-id")
XCTAssertEqual(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.viewID] as? String, "rum-view-id")
XCTAssertNil(log.attributes.internalAttributes?[LogEvent.Attributes.RUM.actionID])
}
// swiftlint:enable multiline_literal_brackets

func testWhenSendingCrashReportWithMalformedRUMContext_itSendsErrorTelemetry() throws {
// Given (CR with the link to RUM view)
let crashContext = crashContextWith(
lastRUMViewEvent: AnyCodable(["rum-view": "malformed"])
)

// When
let telemetry = TelemetryReceiverMock()
let core = PassthroughCoreMock(
messageReceiver: CombinedFeatureMessageReceiver([
CrashLogReceiver(dateProvider: SystemDateProvider()),
telemetry
])
)

let sender = MessageBusSender(core: core)
sender.send(report: crashReport, with: crashContext)

// Then
let error = try XCTUnwrap(telemetry.messages.firstError())
XCTAssertTrue(error.message.hasPrefix("Failed to decode crash message in `LogMessageReceiver`"))
XCTAssertTrue(core.events(ofType: LogEvent.self).isEmpty, "It should send no log")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,16 @@ internal extension DDCrashReport {
}

static func mockRandomWith(context: CrashContext) -> DDCrashReport {
return mockRandomWith(contextData: context.data)
}

static func mockRandomWith(contextData: Data) -> DDCrashReport {
return mockWith(
date: .mockRandomInThePast(),
type: .mockRandom(),
message: .mockRandom(),
stack: .mockRandom(),
context: context.data
context: contextData
)
}
}
Expand Down
Loading

0 comments on commit e01e97b

Please sign in to comment.