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-1833 feat: Link (emergency) crash Logs to crashed RUM session #1645

Merged
merged 3 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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][]

# 2.6.0 / 09-01-2024
Expand Down Expand Up @@ -573,6 +574,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 @@ -2166,6 +2168,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 @@ -3563,6 +3566,7 @@
610ABD492A69309900AFEA34 /* IntegrationUnitTests */ = {
isa = PBXGroup;
children = (
6179DB542B60229D00E9E04E /* CrashReporting */,
61E8C5062B28896100E709B4 /* RUM */,
610ABD4A2A6930AB00AFEA34 /* Public */,
618353BA2A6946F40085F84A /* Internal */,
Expand Down Expand Up @@ -4362,6 +4366,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 @@ -7345,6 +7357,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 @@ -8422,6 +8435,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