From 76f73ae1c00d7e86b6fcc4e7f1913230e92bcd57 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 24 Jan 2024 10:26:28 +0100 Subject: [PATCH 1/3] RUM-1833 Add integration unit test for sendning CR over Logs and RUM --- Datadog/Datadog.xcodeproj/project.pbxproj | 14 +++ .../SendingCrashReportTests.swift | 118 ++++++++++++++++++ .../Mocks/CrashReportingFeatureMocks.swift | 6 +- .../Sources/CrashReporting.swift | 6 +- 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 3b1c031575..57fe0a0cd7 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -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 */; }; @@ -2166,6 +2168,7 @@ 61776CEC273BEA5500F93802 /* DebugRUMSessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugRUMSessionViewController.swift; sourceTree = ""; }; 61776D4D273E6D9F00F93802 /* SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI.swift; sourceTree = ""; }; 61786F7624FCDE04009E6BAB /* RUMDebuggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMDebuggingTests.swift; sourceTree = ""; }; + 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingCrashReportTests.swift; sourceTree = ""; }; 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ObjcAppLaunchHandler.h; sourceTree = ""; }; 6179FFD2254ADB1100556A0B /* ObjcAppLaunchHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ObjcAppLaunchHandler.m; sourceTree = ""; }; 617B953C24BF4D8F00E6F443 /* RUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMMonitorTests.swift; sourceTree = ""; }; @@ -3563,6 +3566,7 @@ 610ABD492A69309900AFEA34 /* IntegrationUnitTests */ = { isa = PBXGroup; children = ( + 6179DB542B60229D00E9E04E /* CrashReporting */, 61E8C5062B28896100E709B4 /* RUM */, 610ABD4A2A6930AB00AFEA34 /* Public */, 618353BA2A6946F40085F84A /* Internal */, @@ -4362,6 +4366,14 @@ path = Helpers; sourceTree = ""; }; + 6179DB542B60229D00E9E04E /* CrashReporting */ = { + isa = PBXGroup; + children = ( + 6179DB552B6022EA00E9E04E /* SendingCrashReportTests.swift */, + ); + path = CrashReporting; + sourceTree = ""; + }; 617B953B24BF4D7300E6F443 /* RUMMonitor */ = { isa = PBXGroup; children = ( @@ -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 */, @@ -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 */, diff --git a/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift new file mode 100644 index 0000000000..1ccdfd40cc --- /dev/null +++ b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift @@ -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) + } +} diff --git a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift index 6baf036a8c..67fa374c59 100644 --- a/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -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 ) } } diff --git a/DatadogCrashReporting/Sources/CrashReporting.swift b/DatadogCrashReporting/Sources/CrashReporting.swift index 5a7e8caee4..dc18fe898d 100644 --- a/DatadogCrashReporting/Sources/CrashReporting.swift +++ b/DatadogCrashReporting/Sources/CrashReporting.swift @@ -21,11 +21,15 @@ import DatadogInternal public final class CrashReporting { /// Initializes the Datadog Crash Reporter. public static func enable(in core: DatadogCoreProtocol = CoreRegistry.default) { + enable(with: PLCrashReporterPlugin(), in: core) + } + + internal static func enable(with plugin: CrashReportingPlugin, in core: DatadogCoreProtocol) { do { let contextProvider = CrashContextCoreProvider() let reporter = CrashReportingFeature( - crashReportingPlugin: PLCrashReporterPlugin(), + crashReportingPlugin: plugin, crashContextProvider: contextProvider, sender: MessageBusSender(core: core), messageReceiver: contextProvider, From fba2615d50957b8b9f0899a4b2c60290276f90da Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 24 Jan 2024 13:37:20 +0100 Subject: [PATCH 2/3] RUM-1833 Link crash reports sent as emergency Log to current RUM session (if available) --- .../Datadog/Logs/CrashLogReceiverTests.swift | 146 ++++++++++++------ DatadogLogs/Sources/Feature/Baggages.swift | 18 --- .../Sources/Feature/MessageReceivers.swift | 47 +++++- DatadogLogs/Sources/Log/LogEventEncoder.swift | 22 +++ DatadogLogs/Sources/RemoteLogger.swift | 14 +- .../Tests/WebViewLogReceiverTests.swift | 2 +- 6 files changed, 171 insertions(+), 78 deletions(-) diff --git a/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift b/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift index 26f8a7f2e9..10f641c7ca 100644 --- a/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/Logs/CrashLogReceiverTests.swift @@ -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( @@ -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( @@ -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, @@ -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") + } } diff --git a/DatadogLogs/Sources/Feature/Baggages.swift b/DatadogLogs/Sources/Feature/Baggages.swift index f567856e04..a4e41c7da1 100644 --- a/DatadogLogs/Sources/Feature/Baggages.swift +++ b/DatadogLogs/Sources/Feature/Baggages.swift @@ -33,13 +33,6 @@ internal struct SpanContext: Decodable { let traceID: String? let spanID: String? - - var internalAttributes: [String: String?] { - [ - CodingKeys.traceID.rawValue: traceID, - CodingKeys.spanID.rawValue: spanID - ] - } } /// The RUM context received from `DatadogCore`. @@ -61,15 +54,4 @@ internal struct RUMContext: Decodable { let viewID: String? /// The ID of current RUM action (standard UUID `String`, lowercased). let userActionID: String? - - var internalAttributes: [String: String] { - var context: [String: String] = [ - "application_id": applicationID, - "session_id": sessionID - ] - - context["view.id"] = viewID - context["user_action.id"] = userActionID - return context - } } diff --git a/DatadogLogs/Sources/Feature/MessageReceivers.swift b/DatadogLogs/Sources/Feature/MessageReceivers.swift index f43ea389f3..daba70b374 100644 --- a/DatadogLogs/Sources/Feature/MessageReceivers.swift +++ b/DatadogLogs/Sources/Feature/MessageReceivers.swift @@ -85,7 +85,7 @@ internal struct LogMessageReceiver: FeatureMessageReceiver { return true } catch { core.telemetry - .error("Fails to decode crash from Logs", error: error) + .error("Failed to decode log message in `LogMessageReceiver`", error: error) } return false @@ -152,6 +152,27 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { let carrierInfo: CarrierInfo? /// Current user information. let userInfo: UserInfo? + + /// A type representing part of last RUM view information required to link crash log with previous RUM session. + /// It mirrors the schema of `RUMViewEvent`, so we can decode it from the last `RUMViewEvent` coded in crash context. + struct PartialRUMViewEvent: Decodable { + struct Application: Decodable { + let id: String + } + struct Session: Decodable { + let id: String + } + struct View: Decodable { + let id: String + } + + let application: Application + let session: Session + let view: View + } + + /// The last RUM view in crashed app process. + let lastRUMViewEvent: PartialRUMViewEvent? } /// Time provider. @@ -171,7 +192,7 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { return send(report: crash.report, with: crash.context, to: core) } catch { core.telemetry - .error("Fails to decode crash from RUM", error: error) + .error("Failed to decode crash message in `LogMessageReceiver`", error: error) } return false } @@ -184,11 +205,17 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { .addingTimeInterval(context.serverTimeOffset) var errorAttributes: [AttributeKey: AttributeValue] = [:] + // Set crash attributes for the error errorAttributes[DDError.threads] = report.threads errorAttributes[DDError.binaryImages] = report.binaryImages errorAttributes[DDError.meta] = report.meta errorAttributes[DDError.wasTruncated] = report.wasTruncated + // Set RUM context if available (so emergency error is linked to the RUM session in Datadog app) + errorAttributes[LogEvent.Attributes.RUM.applicationID] = context.lastRUMViewEvent?.application.id + errorAttributes[LogEvent.Attributes.RUM.sessionID] = context.lastRUMViewEvent?.session.id + errorAttributes[LogEvent.Attributes.RUM.viewID] = context.lastRUMViewEvent?.view.id + let user = context.userInfo let deviceInfo = context.device @@ -225,7 +252,10 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { ), networkConnectionInfo: context.networkConnectionInfo, mobileCarrierInfo: context.carrierInfo, - attributes: .init(userAttributes: [:], internalAttributes: errorAttributes), + attributes: .init( + userAttributes: [:], + internalAttributes: errorAttributes + ), tags: nil ) @@ -278,10 +308,13 @@ internal struct WebViewLogReceiver: FeatureMessageReceiver { if let rum = context.baggages[RUMContext.key] { do { - let context = try rum.decode(type: RUMContext.self) - event.merge(context.internalAttributes) { $1 } + let rum = try rum.decode(type: RUMContext.self) + event[LogEvent.Attributes.RUM.applicationID] = rum.applicationID + event[LogEvent.Attributes.RUM.sessionID] = rum.sessionID + event[LogEvent.Attributes.RUM.viewID] = rum.viewID + event[LogEvent.Attributes.RUM.actionID] = rum.userActionID } catch { - core.telemetry.error("Fails to decode RUM context from Logs", error: error) + core.telemetry.error("Fails to decode RUM context from Logs in `WebViewLogReceiver`", error: error) } } @@ -291,7 +324,7 @@ internal struct WebViewLogReceiver: FeatureMessageReceiver { return true } catch { core.telemetry - .error("Fails to decode browser log", error: error) + .error("Failed to decode browser log in `LogMessageReceiver`", error: error) } return false diff --git a/DatadogLogs/Sources/Log/LogEventEncoder.swift b/DatadogLogs/Sources/Log/LogEventEncoder.swift index 95dc03b3fd..9ed13f106a 100644 --- a/DatadogLogs/Sources/Log/LogEventEncoder.swift +++ b/DatadogLogs/Sources/Log/LogEventEncoder.swift @@ -23,6 +23,28 @@ public struct LogEvent: Encodable { /// Custom attributes associated with a the log event. public struct Attributes { + /// List of log attribute keys used to establish the link between the Log event and the RUM session that it was collected within. + /// Those keys are recognised by Datadog app and used to render the link in web UI. + internal enum RUM { + /// Key referencing the RUM applicaiton ID. + static let applicationID = "application_id" + /// Key referencing the RUM session ID. + static let sessionID = "session_id" + /// Key referencing the RUM view ID. + static let viewID = "view.id" + /// Key referencing the RUM action ID. + static let actionID = "user_action.id" + } + + /// List of log attribute keys used to establish the link between the Log event and the Tracing span that it was collected within. + /// Those keys are recognised by Datadog app and used to render the link in web UI. + internal enum Trace { + /// Key referencing the trace ID. + static let traceID = "dd.trace_id" + /// Key referencing the span ID. + static let spanID = "dd.span_id" + } + /// Log custom attributes, They are subject for sanitization. public var userAttributes: [String: Encodable] /// Log attributes added internally by the SDK. They are not a subject for sanitization. diff --git a/DatadogLogs/Sources/RemoteLogger.swift b/DatadogLogs/Sources/RemoteLogger.swift index 322c63193a..f311383a3b 100644 --- a/DatadogLogs/Sources/RemoteLogger.swift +++ b/DatadogLogs/Sources/RemoteLogger.swift @@ -116,20 +116,26 @@ internal final class RemoteLogger: LoggerProtocol { self.core.scope(for: LogsFeature.name)?.eventWriteContext { context, writer in var internalAttributes: [String: Encodable] = [:] + // When bundle with RUM is enabled, link RUM context (if available): if self.rumContextIntegration, let rum = context.baggages[RUMContext.key] { do { - let attributes = try rum.decode(type: RUMContext.self).internalAttributes - internalAttributes.merge(attributes) { $1 } + let rum = try rum.decode(type: RUMContext.self) + internalAttributes[LogEvent.Attributes.RUM.applicationID] = rum.applicationID + internalAttributes[LogEvent.Attributes.RUM.sessionID] = rum.sessionID + internalAttributes[LogEvent.Attributes.RUM.viewID] = rum.viewID + internalAttributes[LogEvent.Attributes.RUM.actionID] = rum.userActionID } catch { self.core.telemetry .error("Fails to decode RUM context from Logs", error: error) } } + // When bundle with Trace is enabled, link RUM context (if available): if self.activeSpanIntegration, let span = context.baggages[SpanContext.key] { do { - let attributes = try span.decode(type: SpanContext.self).internalAttributes - internalAttributes.merge(attributes) { $1 } + let trace = try span.decode(type: SpanContext.self) + internalAttributes[LogEvent.Attributes.Trace.traceID] = trace.traceID + internalAttributes[LogEvent.Attributes.Trace.spanID] = trace.spanID } catch { self.core.telemetry .error("Fails to decode Span context from Logs", error: error) diff --git a/DatadogLogs/Tests/WebViewLogReceiverTests.swift b/DatadogLogs/Tests/WebViewLogReceiverTests.swift index 263f07bb4d..4266cd6c89 100644 --- a/DatadogLogs/Tests/WebViewLogReceiverTests.swift +++ b/DatadogLogs/Tests/WebViewLogReceiverTests.swift @@ -222,6 +222,6 @@ class WebViewLogReceiverTests: XCTestCase { XCTAssertNil(log["user_action.id"]) let error = try XCTUnwrap(telemetryReceiver.messages.first?.asError) - XCTAssert(error.message.contains("Fails to decode RUM context from Logs - typeMismatch")) + XCTAssert(error.message.contains("Fails to decode RUM context from Logs in `WebViewLogReceiver` - typeMismatch")) } } From 3add863c24614da676f919fc35cba74865bdd25f Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 24 Jan 2024 16:09:15 +0100 Subject: [PATCH 3/3] RUM-1833 Add CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee73155371..46c1c44a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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