From 4edfde255fceda50a85c5223d3ef28218babea7f Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 28 Mar 2024 18:14:06 +0100 Subject: [PATCH 1/4] RUM-3461 Send fatal App Hang after app restart --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 + .../SendingCrashReportTests.swift | 16 +- .../Datadog/Mocks/RUMDataModelMocks.swift | 15 +- .../CrashReportReceiverTests.swift | 123 ++++---- .../Sources/CrashReportingFeature.swift | 2 +- .../Integrations/CrashReportSender.swift | 1 + .../Models/CrashReporting/BinaryImage.swift | 2 +- .../Models/CrashReporting/DDCrashReport.swift | 4 +- .../Models/CrashReporting/DDThread.swift | 2 +- .../Sources/Feature/MessageReceivers.swift | 26 +- .../DataModels/RUMDataModelsMapping.swift | 22 ++ DatadogRUM/Sources/FatalErrorBuilder.swift | 206 ++++++++++++ .../Instrumentation/AppHangs/AppHang.swift | 4 +- .../AppHangs/AppHangsMonitor.swift | 11 +- .../AppHangs/FatalAppHangsHandler.swift | 65 +++- .../Integrations/CrashReportReceiver.swift | 294 ++---------------- .../Sources/Scrubbing/RUMEventsMapper.swift | 5 - .../AppHangs/AppHangsMonitorTests.swift | 32 +- .../Tests/Mocks/RUMDataModelMocks.swift | 13 - .../Scrubbing/RUMEventsMapperTests.swift | 13 - TestUtilities/Mocks/TelemetryMocks.swift | 5 + 21 files changed, 430 insertions(+), 437 deletions(-) create mode 100644 DatadogRUM/Sources/FatalErrorBuilder.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 5ad7a86d47..77ae778d1c 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -491,6 +491,8 @@ 61D3E0E7277B3D92008BE766 /* KronosTimeStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */; }; 61D3E0EA277E0C58008BE766 /* KronosE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */; }; 61DA20F026C40121004AFE6D /* DataUploadStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */; }; + 61DA6F6C2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */; }; + 61DA6F6D2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */; }; 61DA8CA928609C5B0074A606 /* Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CA828609C5B0074A606 /* Directories.swift */; }; 61DA8CAA28609C5B0074A606 /* Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CA828609C5B0074A606 /* Directories.swift */; }; 61DA8CAC2861C3720074A606 /* DirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */; }; @@ -2428,6 +2430,7 @@ 61D3E0E2277B3D92008BE766 /* KronosTimeStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KronosTimeStorageTests.swift; sourceTree = ""; }; 61D3E0E9277E0C58008BE766 /* KronosE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KronosE2ETests.swift; sourceTree = ""; }; 61DA20EF26C40121004AFE6D /* DataUploadStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploadStatusTests.swift; sourceTree = ""; }; + 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorBuilder.swift; sourceTree = ""; }; 61DA8CA828609C5B0074A606 /* Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Directories.swift; sourceTree = ""; }; 61DA8CAB2861C3720074A606 /* DirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoriesTests.swift; sourceTree = ""; }; 61DA8CAE28620C760074A606 /* Cryptography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cryptography.swift; sourceTree = ""; }; @@ -5772,6 +5775,7 @@ 61C713A12A3B78F900FA735A /* RUMMonitorProtocol+Internal.swift */, 61E5333524B84B43003D6C4E /* RUMMonitor.swift */, D25FF2EA29CC6D6F0063802D /* RUMConfiguration.swift */, + 61DA6F6B2BB57E32009537E5 /* FatalErrorBuilder.swift */, D25FF2E629CC6B320063802D /* Feature */, B3FC3C0426526EE900DEED9E /* RUMVitals */, 61E5333224B7A504003D6C4E /* DataModels */, @@ -8269,6 +8273,7 @@ D23F8E6929DDCD28001CFAE8 /* RUMContextAttributes.swift in Sources */, D23F8E6B29DDCD28001CFAE8 /* RUMMonitor.swift in Sources */, D23F8E6C29DDCD28001CFAE8 /* RUMContextProvider.swift in Sources */, + 61DA6F6D2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */, D23F8E6D29DDCD28001CFAE8 /* ViewIdentifier.swift in Sources */, 49D8C0B82AC5D2160075E427 /* RUM+Internal.swift in Sources */, D23F8E6E29DDCD28001CFAE8 /* RUMViewsHandler.swift in Sources */, @@ -8557,6 +8562,7 @@ D29A9F6829DD85BB005C54A4 /* RUMContextAttributes.swift in Sources */, D29A9F6329DD85BB005C54A4 /* RUMMonitor.swift in Sources */, D29A9F7029DD85BB005C54A4 /* RUMContextProvider.swift in Sources */, + 61DA6F6C2BB57E32009537E5 /* FatalErrorBuilder.swift in Sources */, D29A9F6029DD85BB005C54A4 /* ViewIdentifier.swift in Sources */, 49D8C0B72AC5D2160075E427 /* RUM+Internal.swift in Sources */, D29A9F7629DD85BB005C54A4 /* RUMViewsHandler.swift in Sources */, diff --git a/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift index 34d59bc27f..1a048bd2b9 100644 --- a/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift +++ b/Datadog/IntegrationUnitTests/CrashReporting/SendingCrashReportTests.swift @@ -72,14 +72,14 @@ class SendingCrashReportTests: XCTestCase { 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]) + let rumEvent = try XCTUnwrap(core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self).first) + XCTAssertEqual(rumEvent.error.message, crashReport.message) + XCTAssertEqual(rumEvent.error.type, crashReport.type) + XCTAssertEqual(rumEvent.error.stack, crashReport.stack) + XCTAssertNotNil(rumEvent.error.threads) + XCTAssertNotNil(rumEvent.error.binaryImages) + XCTAssertNotNil(rumEvent.error.meta) + XCTAssertNotNil(rumEvent.error.wasTruncated) } func testWhenSendingCrashReportAsLog_itIsLinkedToTheRUMSessionThatHasCrashed() throws { diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift index a2d37b857e..72d2a4c96d 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMDataModelMocks.swift @@ -130,7 +130,7 @@ extension RUMViewEvent: RandomMockable { pageStates: nil, replayStats: nil, session: .init( - plan: [.plan1, .plan2].randomElement()!, + plan: .plan1, sessionPrecondition: .mockRandom() ) ), @@ -432,19 +432,6 @@ extension RUMErrorEvent: RandomMockable { } } -extension RUMCrashEvent: RandomMockable { - static func mockRandom(error: RUMErrorEvent) -> RUMCrashEvent { - return .init( - error: error, - additionalAttributes: mockRandomAttributes() - ) - } - - public static func mockRandom() -> RUMCrashEvent { - return mockRandom(error: .mockRandom()) - } -} - extension RUMLongTaskEvent.DD.Configuration: RandomMockable { public static func mockRandom() -> RUMLongTaskEvent.DD.Configuration { return .init(sessionReplaySampleRate: .mockRandom(min: 0, max: 100), sessionSampleRate: .mockRandom(min: 0, max: 100)) diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index 5a5ff43651..8205676e71 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -9,7 +9,6 @@ import TestUtilities import DatadogInternal @testable import DatadogRUM @testable import DatadogCrashReporting -@testable import DatadogCore class CrashReportReceiverTests: XCTestCase { private let featureScope = FeatureScopeMock() @@ -30,7 +29,7 @@ class CrashReportReceiverTests: XCTestCase { // Then XCTAssertTrue(result, "It must accept the message") - XCTAssertEqual(featureScope.eventsWritten(ofType: RUMCrashEvent.self).count, 1, "It should send error event") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1, "It should send error event") } func testReceiveCrashAndViewEvent() throws { @@ -52,7 +51,7 @@ class CrashReportReceiverTests: XCTestCase { // Then XCTAssertTrue(result, "It must accept the message") - XCTAssertEqual(featureScope.eventsWritten(ofType: RUMCrashEvent.self).count, 1, "It should send error event") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1, "It should send error event") XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1, "It should send view event") } @@ -89,7 +88,7 @@ class CrashReportReceiverTests: XCTestCase { // Then XCTAssertEqual(featureScope.eventsWritten.count, 2, "It must send both RUM error and RUM view") - XCTAssertEqual(featureScope.eventsWritten(ofType: RUMCrashEvent.self).count, 1) + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1) } @@ -157,7 +156,7 @@ class CrashReportReceiverTests: XCTestCase { // Then XCTAssertEqual(featureScope.eventsWritten.count, 1, "It must send only RUM error") - XCTAssertEqual(featureScope.eventsWritten(ofType: RUMCrashEvent.self).count, 1) + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) } func testGivenCrashDuringBackgroundRUMSessionWithNoActiveView_whenSending_itSendsBothRUMErrorAndRUMViewEvent() throws { @@ -191,7 +190,7 @@ class CrashReportReceiverTests: XCTestCase { // Then XCTAssertEqual(featureScope.eventsWritten.count, 2, "It must send both RUM error and RUM view") - XCTAssertEqual(featureScope.eventsWritten(ofType: RUMCrashEvent.self).count, 1) + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1) } @@ -226,7 +225,7 @@ class CrashReportReceiverTests: XCTestCase { // Then XCTAssertEqual(featureScope.eventsWritten.count, 2, "It must send both RUM error and RUM view") - XCTAssertEqual(featureScope.eventsWritten(ofType: RUMCrashEvent.self).count, 1) + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1) } @@ -449,7 +448,7 @@ class CrashReportReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: RelativeDateProvider( using: crashDate.addingTimeInterval( - .mockRandom(min: 10, max: 2 * CrashReportReceiver.Constants.viewEventAvailabilityThreshold) // simulate restarting app from 10s to 8h later + .mockRandom(min: 10, max: 2 * FatalErrorBuilder.Constants.viewEventAvailabilityThreshold) // simulate restarting app from 10s to 8h later ) ), sessionSampler: Bool.random() ? .mockKeepAll() : .mockRejectAll(), // no matter sampling (as previous session was sampled) @@ -465,18 +464,18 @@ class CrashReportReceiverTests: XCTestCase { ) // Then - let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] + let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] XCTAssertTrue( - sendRUMErrorEvent.model.application.id == lastRUMViewEvent.application.id - && sendRUMErrorEvent.model.session.id == lastRUMViewEvent.session.id - && sendRUMErrorEvent.model.view.id == lastRUMViewEvent.view.id, + sendRUMErrorEvent.application.id == lastRUMViewEvent.application.id + && sendRUMErrorEvent.session.id == lastRUMViewEvent.session.id + && sendRUMErrorEvent.view.id == lastRUMViewEvent.view.id, "The `RUMErrorEvent` sent must be linked to the same RUM Session as the last `RUMViewEvent`." ) - XCTAssertEqual(sendRUMErrorEvent.model.view.name, lastRUMViewEvent.view.name, "It must include view attributes") - XCTAssertEqual(sendRUMErrorEvent.model.view.referrer, lastRUMViewEvent.view.referrer, "It must include view attributes") - XCTAssertEqual(sendRUMErrorEvent.model.view.url, lastRUMViewEvent.view.url, "It must include view attributes") + XCTAssertEqual(sendRUMErrorEvent.view.name, lastRUMViewEvent.view.name, "It must include view attributes") + XCTAssertEqual(sendRUMErrorEvent.view.referrer, lastRUMViewEvent.view.referrer, "It must include view attributes") + XCTAssertEqual(sendRUMErrorEvent.view.url, lastRUMViewEvent.view.url, "It must include view attributes") XCTAssertNotNil(sendRUMErrorEvent.context, "It must contain context details") XCTAssertNotNil(lastRUMViewEvent.context, "It must contain context details") @@ -488,34 +487,34 @@ class CrashReportReceiverTests: XCTestCase { ) XCTAssertTrue( - sendRUMErrorEvent.model.error.isCrash == true, "The `RUMErrorEvent` sent must be marked as crash." + sendRUMErrorEvent.error.isCrash == true, "The `RUMErrorEvent` sent must be marked as crash." ) XCTAssertEqual( - sendRUMErrorEvent.model.date, + sendRUMErrorEvent.date, crashDate.addingTimeInterval(dateCorrectionOffset).timeIntervalSince1970.toInt64Milliseconds, "The `RUMErrorEvent` sent must include crash date corrected by current correction offset." ) XCTAssertEqual( - sendRUMErrorEvent.model.error.type, + sendRUMErrorEvent.error.type, "SIG_CODE (SIG_NAME)" ) XCTAssertEqual( - sendRUMErrorEvent.model.error.message, + sendRUMErrorEvent.error.message, "Signal details" ) XCTAssertEqual( - sendRUMErrorEvent.model.error.stack, + sendRUMErrorEvent.error.stack, """ 0: stack-trace line 0 1: stack-trace line 1 2: stack-trace line 2 """ ) - XCTAssertEqual(sendRUMErrorEvent.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.threads]), crashReport.threads) - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.binaryImages]), crashReport.binaryImages) - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.meta]), crashReport.meta) - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.wasTruncated]), crashReport.wasTruncated) + XCTAssertEqual(sendRUMErrorEvent.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + DDAssertJSONEqual(sendRUMErrorEvent.error.threads, crashReport.threads) + DDAssertJSONEqual(sendRUMErrorEvent.error.binaryImages, crashReport.binaryImages) + DDAssertJSONEqual(sendRUMErrorEvent.error.meta, crashReport.meta) + DDAssertJSONEqual(sendRUMErrorEvent.error.wasTruncated, crashReport.wasTruncated) } func testGivenCrashDuringRUMSessionWithActiveViewAndOverridenSourceType_whenSendingRUMViewEvent_itSendsOverrideSourceType() throws { @@ -548,8 +547,8 @@ class CrashReportReceiverTests: XCTestCase { ) // Then - let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] - XCTAssertEqual(sendRUMErrorEvent.model.error.sourceType, .iosIl2cpp, "Must send overridden sourceType") + let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] + XCTAssertEqual(sendRUMErrorEvent.error.sourceType, .iosIl2cpp, "Must send overridden sourceType") } func testGivenCrashDuringRUMSessionWithActiveViewAndEventMapper_whenSendingRUMViewEvent_itSendsMappedEvents() throws { @@ -784,7 +783,7 @@ class CrashReportReceiverTests: XCTestCase { // Then let sentRUMView = featureScope.eventsWritten(ofType: RUMViewEvent.self)[0] - let sentRUMError = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] + let sentRUMError = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] // Assert RUM view properties XCTAssertTrue( @@ -822,34 +821,34 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(sentRUMView.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") // Assert RUM error properties - XCTAssertEqual(sentRUMError.model.application.id, sentRUMView.application.id, "It must be linked to the same application as RUM view") - XCTAssertEqual(sentRUMError.model.session.id, sentRUMView.session.id, "It must be linked to the same session as RUM view") - XCTAssertEqual(sentRUMError.model.view.id, sentRUMView.view.id, "It must be linked to the RUM view") - XCTAssertEqual(sentRUMError.model.source, .init(rawValue: randomSource), "Must support configured sources") - XCTAssertEqual(sentRUMError.model.error.sourceType, .ios, "Must send .ios as the sourceType") + XCTAssertEqual(sentRUMError.application.id, sentRUMView.application.id, "It must be linked to the same application as RUM view") + XCTAssertEqual(sentRUMError.session.id, sentRUMView.session.id, "It must be linked to the same session as RUM view") + XCTAssertEqual(sentRUMError.view.id, sentRUMView.view.id, "It must be linked to the RUM view") + XCTAssertEqual(sentRUMError.source, .init(rawValue: randomSource), "Must support configured sources") + XCTAssertEqual(sentRUMError.error.sourceType, .ios, "Must send .ios as the sourceType") DDAssertReflectionEqual( - sentRUMError.model.connectivity, + sentRUMError.connectivity, RUMConnectivity(networkInfo: randomNetworkConnectionInfo, carrierInfo: randomCarrierInfo), "It must contain connectity info from the moment of crash" ) DDAssertReflectionEqual( - sentRUMError.model.usr, + sentRUMError.usr, RUMUser(userInfo: randomUserInfo), "It must contain user info from the moment of crash" ) - XCTAssertTrue(sentRUMError.model.error.isCrash == true, "RUM error must be marked as crash.") + XCTAssertTrue(sentRUMError.error.isCrash == true, "RUM error must be marked as crash.") XCTAssertEqual( - sentRUMError.model.date, + sentRUMError.date, crashDate.addingTimeInterval(dateCorrectionOffset).timeIntervalSince1970.toInt64Milliseconds, "RUM error must include crash date corrected by current correction offset." ) - XCTAssertEqual(sentRUMError.model.error.type, randomCrashType) - XCTAssertEqual(sentRUMError.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.threads], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.binaryImages], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.meta], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.wasTruncated], "It must contain crash details") - XCTAssertEqual(sentRUMError.model.error.category, .exception, "Crashes are considered exceptions") + XCTAssertEqual(sentRUMError.error.type, randomCrashType) + XCTAssertEqual(sentRUMError.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertNotNil(sentRUMError.error.threads, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.binaryImages, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.meta, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.wasTruncated, "It must contain crash details") + XCTAssertEqual(sentRUMError.error.category, .exception, "Crashes are considered exceptions") XCTAssertNil(sentRUMView.context, "It musn't contain context as there was no last active view") } @@ -920,8 +919,8 @@ class CrashReportReceiverTests: XCTestCase { ) // Then - let sentRUMError = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] - XCTAssertEqual(sentRUMError.model.error.sourceType, .iosIl2cpp, "Must send overridden sourceType") + let sentRUMError = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] + XCTAssertEqual(sentRUMError.error.sourceType, .iosIl2cpp, "Must send overridden sourceType") } try test( @@ -1072,7 +1071,7 @@ class CrashReportReceiverTests: XCTestCase { // Then let sentRUMView = featureScope.eventsWritten(ofType: RUMViewEvent.self)[0] - let sentRUMError = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] + let sentRUMError = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] // Assert RUM view properties XCTAssertTrue( @@ -1106,11 +1105,11 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(sentRUMView.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") // Assert RUM error properties - XCTAssertEqual(sentRUMError.model.application.id, sentRUMView.application.id, "It must be linked to the same application as RUM view") - XCTAssertEqual(sentRUMError.model.session.id, sentRUMView.session.id, "It must be linked to the same session as RUM view") - XCTAssertEqual(sentRUMError.model.view.id, sentRUMView.view.id, "It must be linked to the RUM view") + XCTAssertEqual(sentRUMError.application.id, sentRUMView.application.id, "It must be linked to the same application as RUM view") + XCTAssertEqual(sentRUMError.session.id, sentRUMView.session.id, "It must be linked to the same session as RUM view") + XCTAssertEqual(sentRUMError.view.id, sentRUMView.view.id, "It must be linked to the RUM view") DDAssertReflectionEqual( - sentRUMError.model.connectivity, + sentRUMError.connectivity, RUMConnectivity(networkInfo: randomNetworkConnectionInfo, carrierInfo: randomCarrierInfo), "It must contain connectity info from the moment of crash" ) @@ -1119,19 +1118,19 @@ class CrashReportReceiverTests: XCTestCase { RUMUser(userInfo: randomUserInfo), "It must contain user info from the moment of crash" ) - XCTAssertTrue(sentRUMError.model.error.isCrash == true, "RUM error must be marked as crash.") + XCTAssertTrue(sentRUMError.error.isCrash == true, "RUM error must be marked as crash.") XCTAssertEqual( - sentRUMError.model.date, + sentRUMError.date, crashDate.addingTimeInterval(dateCorrectionOffset).timeIntervalSince1970.toInt64Milliseconds, "RUM error must include crash date corrected by current correction offset." ) - XCTAssertEqual(sentRUMError.model.error.type, randomCrashType) - XCTAssertEqual(sentRUMError.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.threads], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.binaryImages], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.meta], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.wasTruncated], "It must contain crash details") - XCTAssertEqual(sentRUMError.model.error.sourceType, .ios, "Must send .ios as the sourceType") + XCTAssertEqual(sentRUMError.error.type, randomCrashType) + XCTAssertEqual(sentRUMError.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertNotNil(sentRUMError.error.threads, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.binaryImages, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.meta, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.wasTruncated, "It must contain crash details") + XCTAssertEqual(sentRUMError.error.sourceType, .ios, "Must send .ios as the sourceType") } try test( @@ -1195,9 +1194,9 @@ class CrashReportReceiverTests: XCTestCase { ) // Then - let sentRUMError = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] + let sentRUMError = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] - XCTAssertEqual(sentRUMError.model.error.sourceType, .iosIl2cpp, "Must send overridden sourceType") + XCTAssertEqual(sentRUMError.error.sourceType, .iosIl2cpp, "Must send overridden sourceType") } try test( diff --git a/DatadogCrashReporting/Sources/CrashReportingFeature.swift b/DatadogCrashReporting/Sources/CrashReportingFeature.swift index 2d216ce3a7..8d34e42b33 100644 --- a/DatadogCrashReporting/Sources/CrashReportingFeature.swift +++ b/DatadogCrashReporting/Sources/CrashReportingFeature.swift @@ -58,7 +58,7 @@ internal final class CrashReportingFeature: DatadogFeature { queue.async { self.plugin.readPendingCrashReport { [weak self] crashReport in guard let self = self, let availableCrashReport = crashReport else { - DD.logger.debug("No pending crash available") + DD.logger.debug("No pending Crash found") return false } diff --git a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift index bd37ebb9a4..0052cec46b 100644 --- a/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift +++ b/DatadogCrashReporting/Sources/Integrations/CrashReportSender.swift @@ -47,6 +47,7 @@ internal struct MessageBusSender: CrashReportSender { /// - context: The crash context func send(report: DDCrashReport, with context: CrashContext) { guard let core = core, context.trackingConsent == .granted else { + DD.logger.debug("Skipped sending Crash Report as it was recorded with \(context.trackingConsent) consent") return } diff --git a/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift b/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift index 14b31ba2b7..e22ef97166 100644 --- a/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift +++ b/DatadogInternal/Sources/Models/CrashReporting/BinaryImage.swift @@ -7,7 +7,7 @@ import Foundation /// Binary Image referenced in frames from `DDThread`. -public struct BinaryImage: Codable { +public struct BinaryImage: Codable, PassthroughAnyCodable { public let libraryName: String public let uuid: String public let architecture: String diff --git a/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift b/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift index 2b4fa13a62..71cd46a0f6 100644 --- a/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift +++ b/DatadogInternal/Sources/Models/CrashReporting/DDCrashReport.swift @@ -7,10 +7,10 @@ import Foundation /// Crash Report format supported by Datadog SDK. -public struct DDCrashReport: Codable { +public struct DDCrashReport: Codable, PassthroughAnyCodable { /// Meta information about the process. /// Ref.: https://developer.apple.com/documentation/xcode/examining-the-fields-in-a-crash-report - public struct Meta: Codable { + public struct Meta: Codable, PassthroughAnyCodable { /// A client-generated 16-byte UUID of the incident. public let incidentIdentifier: String? /// The name of the crashed process. diff --git a/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift b/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift index c99c69a6ac..4cc10a4ba8 100644 --- a/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift +++ b/DatadogInternal/Sources/Models/CrashReporting/DDThread.swift @@ -7,7 +7,7 @@ import Foundation /// Unsymbolicated stack trace of a running thread. -public struct DDThread: Codable { +public struct DDThread: Codable, PassthroughAnyCodable { /// The name of the thread, e.g. `"Thread 0"` public let name: String /// Unsymbolicated stack trace of the crash. diff --git a/DatadogLogs/Sources/Feature/MessageReceivers.swift b/DatadogLogs/Sources/Feature/MessageReceivers.swift index 64bf0ae3d2..8377c83e7b 100644 --- a/DatadogLogs/Sources/Feature/MessageReceivers.swift +++ b/DatadogLogs/Sources/Feature/MessageReceivers.swift @@ -94,33 +94,11 @@ internal struct LogMessageReceiver: FeatureMessageReceiver { internal struct CrashLogReceiver: FeatureMessageReceiver { private struct Crash: Decodable { /// The crash report. - let report: CrashReport + let report: DDCrashReport /// The crash context let context: CrashContext } - private struct CrashReport: Decodable { - /// 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: AnyCodable - /// List of binary images referenced from all stack traces. - let binaryImages: AnyCodable - /// Meta information about the crash and process. - let meta: AnyCodable - /// If any stack trace information was truncated due to crash report minimization. - let wasTruncated: Bool - } - private struct CrashContext: Decodable { /// Interval between device and server time. let serverTimeOffset: TimeInterval @@ -212,7 +190,7 @@ internal struct CrashLogReceiver: FeatureMessageReceiver { return false } - private func send(report: CrashReport, with crashContext: CrashContext, to core: DatadogCoreProtocol) -> Bool { + private func send(report: DDCrashReport, with crashContext: CrashContext, to core: DatadogCoreProtocol) -> Bool { // The `report.crashDate` uses system `Date` collected at the moment of crash, so we need to adjust it // to the server time before processing. Following use of the current correction is not ideal, but this is the best // approximation we can get. diff --git a/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift b/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift index 0f956b9c2a..f08bdf92ee 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModelsMapping.swift @@ -91,6 +91,10 @@ internal extension DDThread { } } +internal extension Array where Element == DDThread { + var toRUMDataFormat: [RUMErrorEvent.Error.Threads] { map { $0.toRUMDataFormat } } +} + internal extension BinaryImage { var toRUMDataFormat: RUMErrorEvent.Error.BinaryImages { return .init( @@ -103,3 +107,21 @@ internal extension BinaryImage { ) } } + +internal extension Array where Element == BinaryImage { + var toRUMDataFormat: [RUMErrorEvent.Error.BinaryImages] { map { $0.toRUMDataFormat } } +} + +internal extension DDCrashReport.Meta { + var toRUMDataFormat: RUMErrorEvent.Error.Meta { + return .init( + codeType: codeType, + exceptionCodes: exceptionCodes, + exceptionType: exceptionType, + incidentIdentifier: incidentIdentifier, + parentProcess: parentProcess, + path: path, + process: process + ) + } +} diff --git a/DatadogRUM/Sources/FatalErrorBuilder.swift b/DatadogRUM/Sources/FatalErrorBuilder.swift new file mode 100644 index 0000000000..446bd224c8 --- /dev/null +++ b/DatadogRUM/Sources/FatalErrorBuilder.swift @@ -0,0 +1,206 @@ +/* + * 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 Foundation +import DatadogInternal + +/// Builder for constructing fatal errors (such as Crashes or Fatal App Hangs) that can be sent to the last RUM session in previous process. +internal struct FatalErrorBuilder { + struct Constants { + /// Maximum time since the occurrence of fatal error enabling us to send the RUM view event to associate it with the interrupted RUM session: + /// * if the app is restarted earlier than the date of fatal error + this interval, then we send both the `RUMErrorEvent` and `RUMViewEvent`, + /// * if the app is restarted later than the date of fatal error + this interval, then we only send `RUMErrorEvent`. + /// + /// This condition originates from RUM backend constraints on processing `RUMViewEvents` in stale sessions. If the session does not + /// receive any updates for a long time, then sending some significantly later may lead to inconsistency. + static let viewEventAvailabilityThreshold: TimeInterval = 14_400 // 4 hours + } + + enum FatalError { + /// A crash with given metadata information. + case crash + /// A fatal App Hang. + case hang + } + + /// Current SDK context. + let context: DatadogContext + + let error: FatalError + + let errorDate: Date + let errorType: String + let errorMessage: String + let errorStack: String + + let errorThreads: [RUMErrorEvent.Error.Threads]? + let errorBinaryImages: [RUMErrorEvent.Error.BinaryImages]? + let errorWasTruncated: Bool? + let errorMeta: RUMErrorEvent.Error.Meta? + + /// Creates RUM error linked to given view. + func createRUMError(with lastRUMView: RUMViewEvent) -> RUMErrorEvent { + let event = RUMErrorEvent( + dd: .init( + browserSdkVersion: nil, + configuration: lastRUMView.dd.configuration.map { + .init( + sessionReplaySampleRate: $0.sessionReplaySampleRate, + sessionSampleRate: $0.sessionSampleRate + ) + }, + session: .init( + plan: .plan1, + sessionPrecondition: lastRUMView.dd.session?.sessionPrecondition + ) + ), + action: nil, + application: .init(id: lastRUMView.application.id), + buildId: lastRUMView.buildId, + buildVersion: lastRUMView.buildVersion, + ciTest: lastRUMView.ciTest, + connectivity: lastRUMView.connectivity, + container: nil, + context: lastRUMView.context, + date: errorDate.timeIntervalSince1970.toInt64Milliseconds, + device: lastRUMView.device, + display: nil, + error: .init( + binaryImages: errorBinaryImages, + category: { + switch error { + case .crash: return .exception + case .hang: return .appHang + } + }(), + handling: nil, + handlingStack: nil, + id: nil, + isCrash: { + switch error { + case .crash: return true + case .hang: return true // fatal hangs are considered `@error.is_crash: true` + } + }(), + message: errorMessage, + meta: errorMeta, + resource: nil, + source: .source, + sourceType: context.nativeSourceOverride.map { RUMErrorSourceType(rawValue: $0) } ?? .ios, + stack: errorStack, + threads: errorThreads, + type: errorType, + wasTruncated: errorWasTruncated + ), + freeze: nil, // `@error.freeze.duration` is not yet supported for fatal App Hangs + os: lastRUMView.os, + service: lastRUMView.service, + session: .init( + hasReplay: lastRUMView.session.hasReplay, + id: lastRUMView.session.id, + type: lastRUMView.session.type + ), + source: lastRUMView.source?.toErrorEventSource ?? .ios, + synthetics: lastRUMView.synthetics, + usr: lastRUMView.usr, + version: lastRUMView.version, + view: .init( + id: lastRUMView.view.id, + inForeground: nil, + name: lastRUMView.view.name, + referrer: lastRUMView.view.referrer, + url: lastRUMView.view.url + ) + ) + + return event + } + + /// Updates given RUM view with fatal error information. + func updateRUMViewWithError(_ original: RUMViewEvent) -> RUMViewEvent { + return RUMViewEvent( + dd: .init( + browserSdkVersion: original.dd.browserSdkVersion, + configuration: original.dd.configuration, + documentVersion: original.dd.documentVersion + 1, + pageStates: original.dd.pageStates, + replayStats: original.dd.replayStats, + session: original.dd.session + ), + application: original.application, + buildId: original.buildId, + buildVersion: original.buildVersion, + ciTest: original.ciTest, + connectivity: original.connectivity, + container: original.container, + context: original.context, + date: errorDate.timeIntervalSince1970.toInt64Milliseconds - 1, // -1ms to put the fatal error after view in RUM session + device: original.device, + display: original.display, + os: original.os, + privacy: original.privacy, + service: original.service, + session: original.session, + source: original.source ?? .ios, + synthetics: original.synthetics, + usr: original.usr, + version: original.version, + view: .init( + action: original.view.action, + cpuTicksCount: original.view.cpuTicksCount, + cpuTicksPerSecond: original.view.cpuTicksPerSecond, + crash: .init( + count: { + switch error { + case .crash: return 1 + case .hang: return 1 // fatal hangs are considered in `@view.crash.count` + } + }() + ), + cumulativeLayoutShift: original.view.cumulativeLayoutShift, + cumulativeLayoutShiftTargetSelector: original.view.cumulativeLayoutShiftTargetSelector, + customTimings: original.view.customTimings, + domComplete: original.view.domComplete, + domContentLoaded: original.view.domContentLoaded, + domInteractive: original.view.domInteractive, + error: .init( + count: original.view.error.count + 1 // count the new error + ), + firstByte: original.view.firstByte, + firstContentfulPaint: original.view.firstContentfulPaint, + firstInputDelay: original.view.firstInputDelay, + firstInputTargetSelector: original.view.firstInputTargetSelector, + firstInputTime: original.view.firstInputTime, + flutterBuildTime: original.view.flutterBuildTime, + flutterRasterTime: original.view.flutterRasterTime, + frozenFrame: original.view.frozenFrame, + frustration: original.view.frustration, + id: original.view.id, + inForegroundPeriods: original.view.inForegroundPeriods, + interactionToNextPaint: original.view.interactionToNextPaint, + interactionToNextPaintTargetSelector: original.view.interactionToNextPaintTargetSelector, + isActive: false, // after fatal error, this is no longer active view + isSlowRendered: original.view.isSlowRendered, + jsRefreshRate: original.view.jsRefreshRate, + largestContentfulPaint: original.view.largestContentfulPaint, + largestContentfulPaintTargetSelector: original.view.largestContentfulPaintTargetSelector, + loadEvent: original.view.loadEvent, + loadingTime: original.view.loadingTime, + loadingType: original.view.loadingType, + longTask: original.view.longTask, + memoryAverage: original.view.memoryAverage, + memoryMax: original.view.memoryMax, + name: original.view.name, + referrer: original.view.referrer, + refreshRateAverage: original.view.refreshRateAverage, + refreshRateMin: original.view.refreshRateMin, + resource: original.view.resource, + timeSpent: original.view.timeSpent, + url: original.view.url + ) + ) + } +} diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift index b008363d60..4002c1a9a7 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHang.swift @@ -36,8 +36,10 @@ internal struct FatalAppHang: Codable { let processID: UUID /// The actual hang that was recorded. let hang: AppHang - /// Interval between device and server time. + /// Interval between device and server time at the moment of hang's recording. let serverTimeOffset: TimeInterval /// The last RUM view at the moment of hang's recording. let lastRUMView: RUMViewEvent + /// The user's consent at the moment of hang's recording. + let trackingConsent: TrackingConsent } diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift index 765a5d4a07..d053d07e26 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift @@ -20,7 +20,7 @@ internal final class AppHangsMonitor { } /// Watchdog thread that monitors the main queue for App Hangs. - private let watchdogThread: AppHangsObservingThread + private var watchdogThread: AppHangsObservingThread /// Handles non-fatal App Hangs. internal let nonFatalHangsHandler: NonFatalAppHangsHandler /// Handles non-fatal App Hangs. @@ -45,7 +45,8 @@ internal final class AppHangsMonitor { telemetry: featureScope.telemetry ), fatalErrorContext: fatalErrorContext, - processID: processID + processID: processID, + dateProvider: dateProvider ) } @@ -53,14 +54,16 @@ internal final class AppHangsMonitor { featureScope: FeatureScope, watchdogThread: AppHangsObservingThread, fatalErrorContext: FatalErrorContextNotifier, - processID: UUID + processID: UUID, + dateProvider: DateProvider ) { self.watchdogThread = watchdogThread self.nonFatalHangsHandler = NonFatalAppHangsHandler() self.fatalHangsHandler = FatalAppHangsHandler( featureScope: featureScope, fatalErrorContext: fatalErrorContext, - processID: processID + processID: processID, + dateProvider: dateProvider ) } diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift index fd1a341f05..7bf7523666 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift @@ -14,21 +14,25 @@ internal final class FatalAppHangsHandler { private let fatalErrorContext: FatalErrorContextNotifier /// An ID of the current process. private let processID: UUID + /// Device date provider. + private let dateProvider: DateProvider init( featureScope: FeatureScope, fatalErrorContext: FatalErrorContextNotifier, - processID: UUID + processID: UUID, + dateProvider: DateProvider ) { self.featureScope = featureScope self.fatalErrorContext = fatalErrorContext self.processID = processID + self.dateProvider = dateProvider } func startHang(hang: AppHang) { guard let lastRUMView = fatalErrorContext.view else { DD.logger.debug("App Hang is being detected, but won't be considered fatal as there is no active RUM view") - return // expected if there was no active view + return // TODO: RUM-3840 Track fatal App Hangs if there is no active RUM view } featureScope.rumDataStoreContext { [processID] context, dataStore in @@ -36,7 +40,8 @@ internal final class FatalAppHangsHandler { processID: processID, hang: hang, serverTimeOffset: context.serverTimeOffset, - lastRUMView: lastRUMView + lastRUMView: lastRUMView, + trackingConsent: context.trackingConsent ) dataStore.setValue(fatalHang, forKey: .fatalAppHangKey) } @@ -57,22 +62,62 @@ internal final class FatalAppHangsHandler { func reportFatalAppHangIfFound() { featureScope.rumDataStore.value(forKey: .fatalAppHangKey) { [weak self] (fatalHang: FatalAppHang?) in guard let fatalHang = fatalHang else { + DD.logger.debug("No pending App Hang found") return // previous process didn't end up with a hang } guard fatalHang.processID != self?.processID else { return // skip as possible false-positive } - - DD.logger.debug("Loaded fatal App Hang") - self?.send(fatalHang: fatalHang) } } private func send(fatalHang: FatalAppHang) { - // TODO: RUM-3461 - // Similar to how we send Crash report in `CrashReportReceiver`: - // - construct RUM error from `fatalHang.hang` information - // - update `error.count` in `fatalHang.lastRUMView` + guard fatalHang.trackingConsent == .granted else { // consider the user consent from previous session + DD.logger.debug("Skipped sending fatal App Hang as it was recorded with \(fatalHang.trackingConsent) consent") + return + } + + featureScope.eventWriteContext(bypassConsent: true) { [dateProvider] context, writer in // bypass the current consent + // Below we only consider the "happy path" scenario, when fatal App Hang has occurred within an active RUM session + // with an existing active view and the app was restarted in less than `viewEventAvailabilityThreshold` after + // termination. This is only subset of logic implemented for RUM crashes in `RUM.CrashReportReceiver`. + // + // Remaining edge cases include: + // - sending fatal App Hang if there was no active view in previous RUM session (vs BET enabled or not) + // - sending fatal App Hang before RUM session has started (vs "in foreground" or "in background" vs BET enabled or not) + // + // There is an oportunity for covering these cases through massive code reuse between fatal hangs and crashes through `FatalErrorBuilder`. + // TODO: RUM-3840 Track fatal App Hangs if there is no active RUM view + + let builder = FatalErrorBuilder( + context: context, + error: .hang, + errorDate: fatalHang.hang.startDate, + errorType: AppHangsMonitor.Constants.appHangErrorType, + errorMessage: AppHangsMonitor.Constants.appHangErrorMessage, + errorStack: fatalHang.hang.backtraceResult.stack, + errorThreads: fatalHang.hang.backtraceResult.threads?.toRUMDataFormat, + errorBinaryImages: fatalHang.hang.backtraceResult.binaryImages?.toRUMDataFormat, + errorWasTruncated: fatalHang.hang.backtraceResult.wasTruncated, + errorMeta: nil + ) + let error = builder.createRUMError(with: fatalHang.lastRUMView) + let view = builder.updateRUMViewWithError(fatalHang.lastRUMView) + let realErrorDate = fatalHang.hang.startDate.addingTimeInterval(fatalHang.serverTimeOffset) + let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset) + + if realDateNow.timeIntervalSince(realErrorDate) < FatalErrorBuilder.Constants.viewEventAvailabilityThreshold { + DD.logger.debug("Sending fatal App hang as RUM error with issuing RUM view update") + // It is still OK to send RUM view to previous RUM session. + writer.write(value: error) + writer.write(value: view) + } else { + // We know it is too late for sending RUM view to previous RUM session as it is now stale on backend. + // To avoid inconsistency, we only send the RUM error. + DD.logger.debug("Sending fatal App hang as RUM error without updating RUM view") + writer.write(value: error) + } + } } } diff --git a/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift b/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift index c25b2fba0f..2807c1e99d 100644 --- a/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift +++ b/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift @@ -15,45 +15,13 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { static let crash = "crash" } - struct Constants { - /// Maximum time since the crash (in seconds) enabling us to send the RUM View event to associate it with the interrupted RUM Session: - /// * if the app is restarted earlier than crash time + this interval, then we send both the `RUMErrorEvent` and `RUMViewEvent`, - /// * if the app is restarted later than crash time + this interval, then we only send `RUMErrorEvent`. - /// - /// This condition originates from RUM backend constraints on processing `RUMViewEvents` in stale sessions. If the session does not - /// receive any updates for a long time, then sending some significantly later may lead to inconsistency. - static let viewEventAvailabilityThreshold: TimeInterval = 14_400 // 4 hours - } - struct Crash: Decodable { /// The crash report. - let report: CrashReport + let report: DDCrashReport /// The crash context let context: CrashContext } - struct CrashReport: Decodable { - /// 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: AnyCodable - /// List of binary images referenced from all stack traces. - let binaryImages: AnyCodable - /// Meta information about the crash and process. - let meta: AnyCodable - /// If any stack trace information was truncated due to crash report minimization. - let wasTruncated: Bool - } - struct CrashContext: Decodable { /// Interval between device and server time. let serverTimeOffset: TimeInterval @@ -152,7 +120,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { return false } - private func send(report: CrashReport, with context: CrashContext) -> Bool { + private func send(report: DDCrashReport, with context: CrashContext) -> Bool { // The `crashReport.crashDate` uses system `Date` collected at the moment of crash, so we need to adjust it // to the server time before processing. Following use of the current correction is not ideal (it's not the correction // from the moment of crash), but this is the best approximation we can get. @@ -192,18 +160,20 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { /// If the crash occurred in an existing RUM session and we know its `lastRUMViewEvent` we send the error using that session UUID and link /// the crash to that view. The error event can be preceded with a view update based on `Constants.viewEventAvailabilityThreshold` condition. private func sendCrashReportLinkedToLastViewInPreviousSession( - _ crashReport: CrashReport, + _ crashReport: DDCrashReport, lastRUMViewEventInPreviousSession lastRUMViewEvent: RUMViewEvent, using crashTimings: AdjustedCrashTimings ) { - if crashTimings.realDateNow.timeIntervalSince(crashTimings.realCrashDate) < Constants.viewEventAvailabilityThreshold { + if crashTimings.realDateNow.timeIntervalSince(crashTimings.realCrashDate) < FatalErrorBuilder.Constants.viewEventAvailabilityThreshold { send(crashReport: crashReport, to: lastRUMViewEvent, using: crashTimings.realCrashDate) } else { // We know it is too late for sending RUM view to previous RUM session as it is now stale on backend. // To avoid inconsistency, we only send the RUM error. DD.logger.debug("Sending crash as RUM error.") featureScope.eventWriteContext(bypassConsent: true) { context, writer in - let rumError = createRUMError(from: crashReport, and: lastRUMViewEvent, crashDate: crashTimings.realCrashDate, sourceType: context.nativeSourceOverride) + let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: crashTimings.realCrashDate) + let rumError = builder.createRUMError(with: lastRUMViewEvent) + if let mappedError = self.eventsMapper.map(event: rumError) { writer.write(value: mappedError) } else { @@ -218,7 +188,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { /// still send the error using that session UUID. Lack of `lastRUMViewEvent` means that there was no **active** view, but the presence of /// `lastRUMSessionState` indicates that some views were tracked before. private func sendCrashReportToPreviousSession( - _ crashReport: CrashReport, + _ crashReport: DDCrashReport, crashContext: CrashContext, lastRUMSessionStateInPreviousSession lastRUMSessionState: RUMSessionState, using crashTimings: AdjustedCrashTimings @@ -268,7 +238,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { /// If the crash occurred before starting RUM session (after initializing SDK, but before starting the first view) we don't have any session UUID to associate the error with. /// In that case, we consider sending this crash within a new, single-view session: eitherĀ "ApplicationLaunch" view or "Background" view. private func sendCrashReportToNewSession( - _ crashReport: CrashReport, + _ crashReport: DDCrashReport, crashContext: CrashContext, using crashTimings: AdjustedCrashTimings ) { @@ -319,14 +289,15 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { } /// Sends given `CrashReport` by linking it to given `rumView` and updating view counts accordingly. - private func send(crashReport: CrashReport, to rumView: RUMViewEvent, using realCrashDate: Date) { + private func send(crashReport: DDCrashReport, to rumView: RUMViewEvent, using realCrashDate: Date) { DD.logger.debug("Updating RUM view with crash report.") - let updatedRUMView = updateRUMViewWithNewError(rumView, crashDate: realCrashDate) // crash reporting is considering the user consent from previous session, if an event reached // the message bus it means that consent was granted and we can safely bypass current consent. featureScope.eventWriteContext(bypassConsent: true) { context, writer in - let rumError = createRUMError(from: crashReport, and: updatedRUMView, crashDate: realCrashDate, sourceType: context.nativeSourceOverride) + let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: realCrashDate) + let updatedRUMView = builder.updateRUMViewWithError(rumView) + let rumError = builder.createRUMError(with: updatedRUMView) if let mappedError = self.eventsMapper.map(event: rumError) { writer.write(value: mappedError) @@ -342,169 +313,18 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { // MARK: - Building RUM events - /// Creates RUM error based on the session information from `lastRUMViewEvent` and `DDCrashReport` details. - private func createRUMError(from crashReport: CrashReport, and lastRUMView: RUMViewEvent, crashDate: Date, sourceType: String?) -> RUMCrashEvent { - let errorType = crashReport.type - let errorMessage = crashReport.message - let errorStackTrace = crashReport.stack - - var errorAttributes: [String: Encodable] = [:] - errorAttributes[DDError.threads] = crashReport.threads - errorAttributes[DDError.binaryImages] = crashReport.binaryImages - errorAttributes[DDError.meta] = crashReport.meta - errorAttributes[DDError.wasTruncated] = crashReport.wasTruncated - - let rumSourceType: RUMErrorSourceType - if let sourceType = sourceType { - rumSourceType = RUMErrorSourceType(rawValue: sourceType) ?? .ios - } else { - rumSourceType = .ios - } - - let event = RUMErrorEvent( - dd: .init( - browserSdkVersion: nil, - configuration: .init(sessionReplaySampleRate: nil, sessionSampleRate: Double(self.sessionSampler.samplingRate)), - session: .init( - plan: .plan1, - sessionPrecondition: lastRUMView.dd.session?.sessionPrecondition - ) - ), - action: nil, - application: .init(id: lastRUMView.application.id), - buildId: lastRUMView.buildId, - buildVersion: lastRUMView.buildVersion, - ciTest: lastRUMView.ciTest, - connectivity: lastRUMView.connectivity, - container: nil, - context: lastRUMView.context, - date: crashDate.timeIntervalSince1970.toInt64Milliseconds, - device: lastRUMView.device, - display: nil, - error: .init( - binaryImages: nil, - category: .exception, // crashes are categorised as "Exception" - handling: nil, - handlingStack: nil, - id: nil, - isCrash: true, - message: errorMessage, - meta: nil, - resource: nil, - source: .source, - sourceType: rumSourceType, - stack: errorStackTrace, - threads: nil, - type: errorType, - wasTruncated: nil - ), - freeze: nil, - os: lastRUMView.os, - service: lastRUMView.service, - session: .init( - hasReplay: lastRUMView.session.hasReplay, - id: lastRUMView.session.id, - type: lastRUMView.session.type - ), - source: lastRUMView.source?.toErrorEventSource ?? .ios, - synthetics: lastRUMView.synthetics, - usr: lastRUMView.usr, - version: lastRUMView.version, - view: .init( - id: lastRUMView.view.id, - inForeground: nil, - name: lastRUMView.view.name, - referrer: lastRUMView.view.referrer, - url: lastRUMView.view.url - ) - ) - - return RUMCrashEvent( - error: event, - additionalAttributes: errorAttributes - ) - } - - /// Updates given RUM view event with crash information. - private func updateRUMViewWithNewError(_ original: RUMViewEvent, crashDate: Date) -> RUMViewEvent { - return RUMViewEvent( - dd: .init( - browserSdkVersion: nil, - configuration: .init( - sessionReplaySampleRate: nil, - sessionSampleRate: Double(self.sessionSampler.samplingRate), - startSessionReplayRecordingManually: nil - ), - documentVersion: original.dd.documentVersion + 1, - pageStates: nil, - replayStats: nil, - session: .init( - plan: .plan1, - sessionPrecondition: original.dd.session?.sessionPrecondition - ) - ), - application: original.application, - buildId: original.buildId, - buildVersion: original.buildVersion, - ciTest: original.ciTest, - connectivity: original.connectivity, - container: nil, - context: original.context, - date: crashDate.timeIntervalSince1970.toInt64Milliseconds - 1, // -1ms to put the crash after view in RUM session - device: original.device, - display: nil, - os: original.os, - privacy: nil, - service: original.service, - session: original.session, - source: original.source ?? .ios, - synthetics: nil, - usr: original.usr, - version: original.version, - view: .init( - action: original.view.action, - cpuTicksCount: original.view.cpuTicksCount, - cpuTicksPerSecond: original.view.cpuTicksPerSecond, - crash: .init(count: 1), - cumulativeLayoutShift: original.view.cumulativeLayoutShift, - cumulativeLayoutShiftTargetSelector: nil, - customTimings: original.view.customTimings, - domComplete: original.view.domComplete, - domContentLoaded: original.view.domContentLoaded, - domInteractive: original.view.domInteractive, - error: .init(count: original.view.error.count + 1), - firstByte: nil, - firstContentfulPaint: original.view.firstContentfulPaint, - firstInputDelay: original.view.firstInputDelay, - firstInputTargetSelector: nil, - firstInputTime: original.view.firstInputTime, - flutterBuildTime: nil, - flutterRasterTime: nil, - frozenFrame: .init(count: 0), - frustration: .init(count: 0), - id: original.view.id, - inForegroundPeriods: original.view.inForegroundPeriods, - interactionToNextPaint: nil, - interactionToNextPaintTargetSelector: nil, - isActive: false, - isSlowRendered: false, - jsRefreshRate: nil, - largestContentfulPaint: original.view.largestContentfulPaint, - largestContentfulPaintTargetSelector: nil, - loadEvent: original.view.loadEvent, - loadingTime: original.view.loadingTime, - loadingType: original.view.loadingType, - longTask: original.view.longTask, - memoryAverage: original.view.memoryAverage, - memoryMax: original.view.memoryMax, - name: original.view.name, - referrer: original.view.referrer, - refreshRateAverage: original.view.refreshRateAverage, - refreshRateMin: original.view.refreshRateMin, - resource: original.view.resource, - timeSpent: original.view.timeSpent, - url: original.view.url - ) + private func createFatalErrorBuilder(context: DatadogContext, crash: DDCrashReport, crashDate: Date) -> FatalErrorBuilder { + return FatalErrorBuilder( + context: context, + error: .crash, + errorDate: crashDate, + errorType: crash.type, + errorMessage: crash.message, + errorStack: crash.stack, + errorThreads: crash.threads.toRUMDataFormat, + errorBinaryImages: crash.binaryImages.toRUMDataFormat, + errorWasTruncated: crash.wasTruncated, + errorMeta: crash.meta.toRUMDataFormat ) } @@ -615,67 +435,3 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { ) } } - -/// `Encodable` representation of RUM Error event for crash. -/// Mutable properties are subject of sanitization or data scrubbing. -/// TODO: RUMM-1949 - Remove `RUMCrashEvent` with generated model. -internal struct RUMCrashEvent: RUMDataModel { - /// The actual RUM event model created by `RUMMonitor` - var model: RUMErrorEvent - - /// Error attributes. Only set when `DM == RUMErrorEvent` and error describes a crash. - /// Can be entirely removed when RUMM-1463 is resolved and error values are part of the `RUMErrorEvent`. - let additionalAttributes: [String: Encodable]? - - /// Creates a RUM Event object object based on the given sanitizable model. - /// - /// The error attributes keys must be prefixed by `error.*`. - /// - /// - Parameters: - /// - model: The sanitizable event model. - /// - errorAttributes: The optional error attributes. - init(error: RUMErrorEvent, additionalAttributes: [String: Encodable]? = nil) { - self.model = error - self.additionalAttributes = additionalAttributes - } - - func encode(to encoder: Encoder) throws { - // Encode attributes - var container = encoder.container(keyedBy: DynamicCodingKey.self) - - // TODO: RUMM-1463 Remove this `errorAttributes` property once new error format is managed through `RUMDataModels` - try additionalAttributes?.forEach { attribute in - try container.encode(AnyEncodable(attribute.value), forKey: DynamicCodingKey(attribute.key)) - } - - // Encode the sanitized `RUMErrorEvent`. - try model.encode(to: encoder) - } - - init(from decoder: Decoder) throws { - self.model = try RUMErrorEvent(from: decoder) - - // Decode other properties into additionalAttributes - let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) - let dynamicKeys = dynamicContainer.allKeys.filter { RUMErrorEvent.CodingKeys(rawValue: $0.stringValue) == nil } - var dictionary: [String: Codable] = [:] - - try dynamicKeys.forEach { codingKey in - dictionary[codingKey.stringValue] = try dynamicContainer.decode(AnyCodable.self, forKey: codingKey) - } - - self.additionalAttributes = dictionary - } -} - -extension RUMCrashEvent: RUMSanitizableEvent { - var usr: RUMUser? { - get { model.usr } - set { model.usr = newValue } - } - - var context: RUMEventAttributes? { - get { model.context } - set { model.context = newValue } - } -} diff --git a/DatadogRUM/Sources/Scrubbing/RUMEventsMapper.swift b/DatadogRUM/Sources/Scrubbing/RUMEventsMapper.swift index 524ae30111..33c9137cf3 100644 --- a/DatadogRUM/Sources/Scrubbing/RUMEventsMapper.swift +++ b/DatadogRUM/Sources/Scrubbing/RUMEventsMapper.swift @@ -27,11 +27,6 @@ internal struct RUMEventsMapper { return map(event: event, using: viewEventMapper) as? T case let event as RUMErrorEvent: return map(event: event, using: errorEventMapper) as? T - case let event as RUMCrashEvent: - guard let model = map(event: event.model, using: errorEventMapper) else { - return nil - } - return RUMCrashEvent(error: model, additionalAttributes: event.additionalAttributes) as? T case let event as RUMResourceEvent: return map(event: event, using: resourceEventMapper) as? T case let event as RUMActionEvent: diff --git a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift index e582367a61..8d9be38b45 100644 --- a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift +++ b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift @@ -26,6 +26,7 @@ class AppHangsMonitorTests: XCTestCase { private let watchdogThread = WatchdogThreadMock() private let fatalErrorContext = FatalErrorContextNotifier(messageBus: NOPFeatureScope()) private let currentProcessID = UUID() + private let dateProvider = DateProviderMock() private var dd: DDMock! // swiftlint:disable:this implicitly_unwrapped_optional private var monitor: AppHangsMonitor! // swiftlint:disable:this implicitly_unwrapped_optional @@ -35,7 +36,8 @@ class AppHangsMonitorTests: XCTestCase { featureScope: featureScope, watchdogThread: watchdogThread, fatalErrorContext: fatalErrorContext, - processID: currentProcessID + processID: currentProcessID, + dateProvider: dateProvider ) } @@ -97,7 +99,7 @@ class AppHangsMonitorTests: XCTestCase { // Then XCTAssertNotNil(featureScope.dataStoreMock.value(forKey: RUMDataStore.Key.fatalAppHangKey.rawValue)) - XCTAssertTrue(dd.logger.recordedLogs.isEmpty, "It must log no issues") + XCTAssertEqual(dd.logger.debugMessages, ["No pending App Hang found"]) } func testGivenFatalErrorViewContextNotAvailable_whenAppHangStarts_itLogsDebug() throws { @@ -113,8 +115,11 @@ class AppHangsMonitorTests: XCTestCase { // Then XCTAssertNil(featureScope.dataStoreMock.value(forKey: RUMDataStore.Key.fatalAppHangKey.rawValue)) XCTAssertEqual( - dd.logger.debugLog?.message, - "App Hang is being detected, but won't be considered fatal as there is no active RUM view" + dd.logger.debugMessages, + [ + "No pending App Hang found", + "App Hang is being detected, but won't be considered fatal as there is no active RUM view" + ] ) } @@ -131,8 +136,7 @@ class AppHangsMonitorTests: XCTestCase { // Then XCTAssertNil(featureScope.dataStoreMock.value(forKey: RUMDataStore.Key.fatalAppHangKey.rawValue)) - XCTAssertTrue(dd.logger.recordedLogs.isEmpty) - XCTAssertTrue(dd.logger.recordedLogs.isEmpty, "It must log no issues") + XCTAssertEqual(dd.logger.debugLog?.message, "No pending App Hang found") } func testWhenAppHangEnds_itDeletesPendingAppHangInDataStore() throws { @@ -149,7 +153,7 @@ class AppHangsMonitorTests: XCTestCase { // Then XCTAssertNil(featureScope.dataStoreMock.value(forKey: RUMDataStore.Key.fatalAppHangKey.rawValue)) - XCTAssertTrue(dd.logger.recordedLogs.isEmpty, "It must log no issues") + XCTAssertEqual(dd.logger.debugMessages, ["No pending App Hang found"]) } func testGivenPendingHangSavedInOneProcess_whenStartedInDiffferentProcess_itSendsFatalHang() throws { @@ -158,6 +162,7 @@ class AppHangsMonitorTests: XCTestCase { let hang: AppHang = .mockRandom() // Given + featureScope.contextMock.trackingConsent = .granted monitor.start() fatalErrorContext.sessionState = sessionState fatalErrorContext.view = view @@ -165,17 +170,26 @@ class AppHangsMonitorTests: XCTestCase { monitor.stop() // When + featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session + let monitor = AppHangsMonitor( featureScope: featureScope, watchdogThread: watchdogThread, fatalErrorContext: fatalErrorContext, - processID: UUID() // different process + processID: UUID(), // different process + dateProvider: DateProviderMock() ) monitor.start() defer { monitor.stop() } // Then - XCTAssertEqual(dd.logger.debugLog?.message, "Loaded fatal App Hang") + XCTAssertEqual( + dd.logger.debugMessages, + [ + "No pending App Hang found", + "Sending fatal App hang as RUM error without updating RUM view", + ] + ) // TODO: RUM-3461 // Assert on collected RUM error and RUM view update, similar to how we test it for crash reports diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index 4152a03a37..f4c05e0591 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -445,19 +445,6 @@ extension RUMErrorEvent: RandomMockable { } } -extension RUMCrashEvent: RandomMockable { - static func mockRandom(error: RUMErrorEvent) -> RUMCrashEvent { - return .init( - error: error, - additionalAttributes: mockRandomAttributes() - ) - } - - public static func mockRandom() -> RUMCrashEvent { - return mockRandom(error: .mockRandom()) - } -} - extension RUMLongTaskEvent.DD.Configuration: RandomMockable { public static func mockRandom() -> RUMLongTaskEvent.DD.Configuration { return .init(sessionReplaySampleRate: .mockRandom(min: 0, max: 100), sessionSampleRate: .mockRandom(min: 0, max: 100)) diff --git a/DatadogRUM/Tests/Scrubbing/RUMEventsMapperTests.swift b/DatadogRUM/Tests/Scrubbing/RUMEventsMapperTests.swift index 9cf7719448..0750fa1b47 100644 --- a/DatadogRUM/Tests/Scrubbing/RUMEventsMapperTests.swift +++ b/DatadogRUM/Tests/Scrubbing/RUMEventsMapperTests.swift @@ -17,8 +17,6 @@ class RUMEventsMapperTests: XCTestCase { let originalErrorEvent: RUMErrorEvent = .mockRandom() let modifiedErrorEvent: RUMErrorEvent = .mockRandom() - let originalCrashEvent: RUMCrashEvent = .mockRandom(error: originalErrorEvent) - let originalResourceEvent: RUMResourceEvent = .mockRandom() let modifiedResourceEvent: RUMResourceEvent = .mockRandom() @@ -55,7 +53,6 @@ class RUMEventsMapperTests: XCTestCase { // When let mappedViewEvent = mapper.map(event: originalViewEvent) let mappedErrorEvent = mapper.map(event: originalErrorEvent) - let mappedCrashEvent = mapper.map(event: originalCrashEvent) let mappedResourceEvent = mapper.map(event: originalResourceEvent) let mappedActionEvent = mapper.map(event: originalActionEvent) let mappedLongTaskEvent = mapper.map(event: originalLongTaskEvent) @@ -66,13 +63,6 @@ class RUMEventsMapperTests: XCTestCase { DDAssertReflectionEqual(try XCTUnwrap(mappedResourceEvent), modifiedResourceEvent, "Mapper should return modified event.") DDAssertReflectionEqual(try XCTUnwrap(mappedActionEvent), modifiedActionEvent, "Mapper should return modified event.") DDAssertReflectionEqual(try XCTUnwrap(mappedLongTaskEvent), modifiedLongTaskEvent, "Mapper should return modified event.") - - DDAssertReflectionEqual(try XCTUnwrap(mappedCrashEvent?.model), modifiedErrorEvent, "Mapper should return modified event.") - DDAssertDictionariesEqual( - try XCTUnwrap(mappedCrashEvent?.additionalAttributes), - originalCrashEvent.additionalAttributes ?? [:], - "Mapper should return unmodified event attributes." - ) } func testGivenMappersEnabled_whenDroppingEvents_itReturnsNil() { @@ -116,7 +106,6 @@ class RUMEventsMapperTests: XCTestCase { func testGivenMappersDisabled_whenMappingEvents_itReturnsTheirOriginalRepresentation() throws { let originalViewEvent: RUMViewEvent = .mockRandom() - let originalCrashEvent: RUMCrashEvent = .mockRandom() let originalErrorEvent: RUMErrorEvent = .mockRandom() let originalResourceEvent: RUMResourceEvent = .mockRandom() let originalActionEvent: RUMActionEvent = .mockRandom() @@ -128,7 +117,6 @@ class RUMEventsMapperTests: XCTestCase { // When let mappedViewEvent = mapper.map(event: originalViewEvent) let mappedErrorEvent = mapper.map(event: originalErrorEvent) - let mappedCrashEvent = mapper.map(event: originalCrashEvent) let mappedResourceEvent = mapper.map(event: originalResourceEvent) let mappedActionEvent = mapper.map(event: originalActionEvent) let mappedLongTaskEvent = mapper.map(event: originalLongTaskEvent) @@ -136,7 +124,6 @@ class RUMEventsMapperTests: XCTestCase { // Then DDAssertReflectionEqual(try XCTUnwrap(mappedViewEvent), originalViewEvent, "Mapper should return the original event.") DDAssertReflectionEqual(try XCTUnwrap(mappedErrorEvent), originalErrorEvent, "Mapper should return the original event.") - DDAssertReflectionEqual(try XCTUnwrap(mappedCrashEvent), originalCrashEvent, "Mapper should return the original event.") DDAssertReflectionEqual(try XCTUnwrap(mappedResourceEvent), originalResourceEvent, "Mapper should return the original event.") DDAssertReflectionEqual(try XCTUnwrap(mappedActionEvent), originalActionEvent, "Mapper should return the original event.") DDAssertReflectionEqual(try XCTUnwrap(mappedLongTaskEvent), originalLongTaskEvent, "Mapper should return the original event.") diff --git a/TestUtilities/Mocks/TelemetryMocks.swift b/TestUtilities/Mocks/TelemetryMocks.swift index 667134ab0f..91c037985d 100644 --- a/TestUtilities/Mocks/TelemetryMocks.swift +++ b/TestUtilities/Mocks/TelemetryMocks.swift @@ -40,6 +40,11 @@ public class CoreLoggerMock: CoreLogger { public var errorLogs: [RecordedLog] { recordedLogs(ofLevel: .error) } public var criticalLogs: [RecordedLog] { recordedLogs(ofLevel: .critical) } + public var debugMessages: [String] { debugLogs.map { $0.message } } + public var warnMessages: [String] { warnLogs.map { $0.message } } + public var errorMessages: [String] { errorLogs.map { $0.message } } + public var criticalMessages: [String] { criticalLogs.map { $0.message } } + public var debugLog: RecordedLog? { debugLogs.last } public var warnLog: RecordedLog? { warnLogs.last } public var errorLog: RecordedLog? { errorLogs.last } From 66a117d94513ef8e8d3d86cbdf13668de1173065 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 5 Apr 2024 10:20:27 +0200 Subject: [PATCH 2/4] RUM-3461 Test data and conditions behind App Hang uploads --- .../AppHangs/FatalAppHangsHandler.swift | 7 +- .../AppHangs/AppHangsMonitorTests.swift | 236 +++++++++++++++++- .../Tests/Mocks/RUMDataModelMocks.swift | 2 +- 3 files changed, 233 insertions(+), 12 deletions(-) diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift index 7bf7523666..ac5df9068c 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift @@ -90,10 +90,13 @@ internal final class FatalAppHangsHandler { // There is an oportunity for covering these cases through massive code reuse between fatal hangs and crashes through `FatalErrorBuilder`. // TODO: RUM-3840 Track fatal App Hangs if there is no active RUM view + let realErrorDate = fatalHang.hang.startDate.addingTimeInterval(fatalHang.serverTimeOffset) + let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset) + let builder = FatalErrorBuilder( context: context, error: .hang, - errorDate: fatalHang.hang.startDate, + errorDate: realErrorDate, errorType: AppHangsMonitor.Constants.appHangErrorType, errorMessage: AppHangsMonitor.Constants.appHangErrorMessage, errorStack: fatalHang.hang.backtraceResult.stack, @@ -104,8 +107,6 @@ internal final class FatalAppHangsHandler { ) let error = builder.createRUMError(with: fatalHang.lastRUMView) let view = builder.updateRUMViewWithError(fatalHang.lastRUMView) - let realErrorDate = fatalHang.hang.startDate.addingTimeInterval(fatalHang.serverTimeOffset) - let realDateNow = dateProvider.now.addingTimeInterval(context.serverTimeOffset) if realDateNow.timeIntervalSince(realErrorDate) < FatalErrorBuilder.Constants.viewEventAvailabilityThreshold { DD.logger.debug("Sending fatal App hang as RUM error with issuing RUM view update") diff --git a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift index 8d9be38b45..0aabdba81c 100644 --- a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift +++ b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift @@ -156,15 +156,95 @@ class AppHangsMonitorTests: XCTestCase { XCTAssertEqual(dd.logger.debugMessages, ["No pending App Hang found"]) } - func testGivenPendingHangSavedInOneProcess_whenStartedInDiffferentProcess_itSendsFatalHang() throws { - let sessionState: RUMSessionState = .mockRandom() + // MARK: - Fatal App Hangs - Testing Conditional Uploads + + func testGivenPendingHangStartedLessThan4HoursAgo_whenStartedInAnotherProcess_itSendsBothRUMErrorAndRUMViewEvent() throws { + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours)) let view: RUMViewEvent = .mockRandom() - let hang: AppHang = .mockRandom() + let hang: AppHang = .mockWith(startDate: hangDate) + + // Given + featureScope.contextMock.trackingConsent = .granted + monitor.start() + fatalErrorContext.view = view + watchdogThread.delegate?.hangStarted(hang) + monitor.stop() + + // When + featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session + + let monitor = AppHangsMonitor( + featureScope: featureScope, + watchdogThread: watchdogThread, + fatalErrorContext: fatalErrorContext, + processID: UUID(), // different process + dateProvider: DateProviderMock(now: currentDate) + ) + monitor.start() + defer { monitor.stop() } + + // Then + XCTAssertEqual( + dd.logger.debugMessages, + [ + "No pending App Hang found", // from hanged process + "Sending fatal App hang as RUM error with issuing RUM view update", // from next process + ] + ) + + XCTAssertEqual(featureScope.eventsWritten.count, 2, "It must send both RUM error and RUM view") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMViewEvent.self).count, 1) + } + + func testGivenPendingHangStartedMoreThan4HoursAgo_whenStartedInAnotherProcess_itSendsOnlyRUMError() throws { + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let hangDate: Date = currentDate.secondsAgo(.random(in: 4.hours..<24.hours)) + let view: RUMViewEvent = .mockRandom() + let hang: AppHang = .mockWith(startDate: hangDate) // Given featureScope.contextMock.trackingConsent = .granted monitor.start() - fatalErrorContext.sessionState = sessionState + fatalErrorContext.view = view + watchdogThread.delegate?.hangStarted(hang) + monitor.stop() + + // When + featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session + + let monitor = AppHangsMonitor( + featureScope: featureScope, + watchdogThread: watchdogThread, + fatalErrorContext: fatalErrorContext, + processID: UUID(), // different process + dateProvider: DateProviderMock(now: currentDate) + ) + monitor.start() + defer { monitor.stop() } + + // Then + XCTAssertEqual( + dd.logger.debugMessages, + [ + "No pending App Hang found", // from hanged process + "Sending fatal App hang as RUM error without updating RUM view", // from next process + ] + ) + + XCTAssertEqual(featureScope.eventsWritten.count, 1, "It must send only RUM error") + XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) + } + + func testGivenPendingHangStartedWithPendingOrNotGrantedConsent_whenStartedInAnotherProcess_itSendsNoEvent() throws { + let consent: TrackingConsent = .mockRandom(otherThan: TrackingConsent.granted) + let view: RUMViewEvent = .mockRandom() + let hang: AppHang = .mockRandom() + + // Given + featureScope.contextMock.trackingConsent = consent + monitor.start() fatalErrorContext.view = view watchdogThread.delegate?.hangStarted(hang) monitor.stop() @@ -186,12 +266,152 @@ class AppHangsMonitorTests: XCTestCase { XCTAssertEqual( dd.logger.debugMessages, [ - "No pending App Hang found", - "Sending fatal App hang as RUM error without updating RUM view", + "No pending App Hang found", // from hanged process + "Skipped sending fatal App Hang as it was recorded with \(consent) consent", // from next process ] ) - // TODO: RUM-3461 - // Assert on collected RUM error and RUM view update, similar to how we test it for crash reports + XCTAssertEqual(featureScope.eventsWritten.count, 0, "It must send no event") + } + + // MARK: - Fatal App Hangs - Testing Uploaded Data + + func testWhenSendingRUMViewEvent_itIsLinkedToPreviousRUMSessionAndIncludesErrorInformation() throws { + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours)) + let serverTimeOffset: TimeInterval = .mockRandom() + let lastView: RUMViewEvent = .mockRandom() + let hang: AppHang = .mockWith(startDate: hangDate) + + // Given + featureScope.contextMock.trackingConsent = .granted + featureScope.contextMock.serverTimeOffset = serverTimeOffset + monitor.start() + fatalErrorContext.view = lastView + watchdogThread.delegate?.hangStarted(hang) + monitor.stop() + + // When + featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session + + let monitor = AppHangsMonitor( + featureScope: featureScope, + watchdogThread: watchdogThread, + fatalErrorContext: fatalErrorContext, + processID: UUID(), // different process + dateProvider: DateProviderMock(now: currentDate) + ) + monitor.start() + defer { monitor.stop() } + + // Then + let viewEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMViewEvent.self).first) + XCTAssertEqual(viewEvent.application.id, lastView.application.id) + XCTAssertEqual(viewEvent.session.id, lastView.session.id) + XCTAssertEqual(viewEvent.view.id, lastView.view.id) + XCTAssertEqual(viewEvent.dd.documentVersion, lastView.dd.documentVersion + 1, "It must increment document version") + XCTAssertEqual(viewEvent.view.error.count, lastView.view.error.count + 1, "It must count the hang error") + XCTAssertEqual(viewEvent.view.crash?.count, 1, "It must count crash") + XCTAssertEqual(viewEvent.view.isActive, false, "The view must be marked as inactive.") + + XCTAssertEqual(viewEvent.service, lastView.service) + XCTAssertEqual(viewEvent.version, lastView.version) + XCTAssertEqual(viewEvent.buildVersion, lastView.buildVersion) + XCTAssertEqual(viewEvent.view.name, lastView.view.name) + XCTAssertEqual(viewEvent.view.url, lastView.view.url) + XCTAssertEqual(viewEvent.view.resource.count, lastView.view.resource.count) + XCTAssertEqual(viewEvent.view.action.count, lastView.view.action.count) + XCTAssertEqual( + viewEvent.date, + hangDate.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds - 1, + "It must be issued at hang date corrected by recorded offset and shifted back by 1ms" + ) + XCTAssertEqual(viewEvent.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + DDAssertReflectionEqual(viewEvent.device, lastView.device) + DDAssertReflectionEqual(viewEvent.os, lastView.os) + DDAssertJSONEqual(viewEvent.connectivity, lastView.connectivity) + DDAssertJSONEqual(viewEvent.usr, lastView.usr) + } + + func testWhenSendingRUMErrorEvent_itIsLinkedToPreviousRUMSessionAndIncludesErrorInformation() throws { + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours)) + let serverTimeOffset: TimeInterval = .mockRandom() + let lastView: RUMViewEvent = .mockRandom() + let hangBacktrace: BacktraceReport = .mockWith( + stack: """ + 0: stack-trace line 0 + 1: stack-trace line 1 + 2: stack-trace line 2 + """, + 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"), + ], + wasTruncated: .random() + ) + let hang: AppHang = .mockWith(startDate: hangDate, backtraceResult: .succeeded(hangBacktrace)) + + // Given + featureScope.contextMock.trackingConsent = .granted + featureScope.contextMock.serverTimeOffset = serverTimeOffset + monitor.start() + fatalErrorContext.view = lastView + watchdogThread.delegate?.hangStarted(hang) + monitor.stop() + + // When + featureScope.contextMock.trackingConsent = .mockRandom() // no matter of the consent in restarted session + + let monitor = AppHangsMonitor( + featureScope: featureScope, + watchdogThread: watchdogThread, + fatalErrorContext: fatalErrorContext, + processID: UUID(), // different process + dateProvider: DateProviderMock(now: currentDate) + ) + monitor.start() + defer { monitor.stop() } + + // Then + let errorEvent = try XCTUnwrap(featureScope.eventsWritten(ofType: RUMErrorEvent.self).first) + XCTAssertEqual(errorEvent.application.id, lastView.application.id) + XCTAssertEqual(errorEvent.session.id, lastView.session.id) + XCTAssertEqual(errorEvent.view.id, lastView.view.id) + XCTAssertEqual(errorEvent.error.category, .appHang) + XCTAssertEqual(errorEvent.error.isCrash, true, "Fatal hang must be marked as crash") + XCTAssertEqual(errorEvent.view.name, lastView.view.name, "It must include view attributes") + XCTAssertEqual(errorEvent.view.referrer, lastView.view.referrer, "It must include view attributes") + XCTAssertEqual(errorEvent.view.url, lastView.view.url, "It must include view attributes") + DDAssertJSONEqual( + AnyEncodable(errorEvent.context?.contextInfo), + AnyEncodable(lastView.context?.contextInfo), + "It must include the user context from the last view" + ) + XCTAssertEqual( + errorEvent.date, + hangDate.addingTimeInterval(serverTimeOffset).timeIntervalSince1970.toInt64Milliseconds, + "It must include error date corrected by recorded server time offset" + ) + XCTAssertEqual(errorEvent.error.type, AppHangsMonitor.Constants.appHangErrorType) + XCTAssertEqual(errorEvent.error.message, AppHangsMonitor.Constants.appHangErrorMessage) + XCTAssertEqual( + errorEvent.error.stack, + """ + 0: stack-trace line 0 + 1: stack-trace line 1 + 2: stack-trace line 2 + """ + ) + XCTAssertEqual(errorEvent.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + DDAssertJSONEqual(errorEvent.error.threads, hangBacktrace.threads.toRUMDataFormat) + DDAssertJSONEqual(errorEvent.error.binaryImages, hangBacktrace.binaryImages.toRUMDataFormat) + XCTAssertEqual(errorEvent.error.wasTruncated, hangBacktrace.wasTruncated) } } diff --git a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift index f4c05e0591..b6020bc788 100644 --- a/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMDataModelMocks.swift @@ -143,7 +143,7 @@ extension RUMViewEvent: RandomMockable { pageStates: nil, replayStats: nil, session: .init( - plan: [.plan1, .plan2].randomElement()!, + plan: .plan1, sessionPrecondition: .mockRandom() ) ), From 9515f368a3590f3b81c162caf43ab84ff9545de4 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 5 Apr 2024 10:59:04 +0200 Subject: [PATCH 3/4] RUM-3461 Lint --- DatadogRUM/Sources/Integrations/CrashReportReceiver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift b/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift index 2807c1e99d..80d0e1fc07 100644 --- a/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift +++ b/DatadogRUM/Sources/Integrations/CrashReportReceiver.swift @@ -173,7 +173,7 @@ internal struct CrashReportReceiver: FeatureMessageReceiver { featureScope.eventWriteContext(bypassConsent: true) { context, writer in let builder = createFatalErrorBuilder(context: context, crash: crashReport, crashDate: crashTimings.realCrashDate) let rumError = builder.createRUMError(with: lastRUMViewEvent) - + if let mappedError = self.eventsMapper.map(event: rumError) { writer.write(value: mappedError) } else { From 379314d7a0934d6cfce18cad448da1202cd68d7b Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 5 Apr 2024 12:22:50 +0200 Subject: [PATCH 4/4] RUM-3461 Fix rebase issues --- .../CrashReportReceiverTests.swift | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index 8205676e71..73e174a98c 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -596,8 +596,8 @@ class CrashReportReceiverTests: XCTestCase { let sendRUMViewEvent = featureScope.eventsWritten(ofType: RUMViewEvent.self).last XCTAssertEqual(sendRUMViewEvent?.view.name, modifiedViewName, "Must send event mapper modified view event") - let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] - XCTAssertEqual(sendRUMErrorEvent.model.error.fingerprint, errorFingerprint, "Must send event mapper modified error event") + let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] + XCTAssertEqual(sendRUMErrorEvent.error.fingerprint, errorFingerprint, "Must send event mapper modified error event") } func testGivenCrashDuringRUMSessionWithActiveView_whenErrorMapperReturnsNull_itSendOriginalError() throws { @@ -646,7 +646,7 @@ class CrashReportReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: RelativeDateProvider( using: crashDate.addingTimeInterval( - .mockRandom(min: 10, max: 2 * CrashReportReceiver.Constants.viewEventAvailabilityThreshold) // simulate restarting app from 10s to 8h later + .mockRandom(min: 10, max: 2 * FatalErrorBuilder.Constants.viewEventAvailabilityThreshold) // simulate restarting app from 10s to 8h later ) ), sessionSampler: Bool.random() ? .mockKeepAll() : .mockRejectAll(), // no matter sampling (as previous session was sampled) @@ -667,18 +667,18 @@ class CrashReportReceiverTests: XCTestCase { ) // Then - let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] + let sendRUMErrorEvent = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] XCTAssertTrue( - sendRUMErrorEvent.model.application.id == lastRUMViewEvent.application.id - && sendRUMErrorEvent.model.session.id == lastRUMViewEvent.session.id - && sendRUMErrorEvent.model.view.id == lastRUMViewEvent.view.id, + sendRUMErrorEvent.application.id == lastRUMViewEvent.application.id + && sendRUMErrorEvent.session.id == lastRUMViewEvent.session.id + && sendRUMErrorEvent.view.id == lastRUMViewEvent.view.id, "The `RUMErrorEvent` sent must be linked to the same RUM Session as the last `RUMViewEvent`." ) - XCTAssertEqual(sendRUMErrorEvent.model.view.name, lastRUMViewEvent.view.name, "It must include view attributes") - XCTAssertEqual(sendRUMErrorEvent.model.view.referrer, lastRUMViewEvent.view.referrer, "It must include view attributes") - XCTAssertEqual(sendRUMErrorEvent.model.view.url, lastRUMViewEvent.view.url, "It must include view attributes") + XCTAssertEqual(sendRUMErrorEvent.view.name, lastRUMViewEvent.view.name, "It must include view attributes") + XCTAssertEqual(sendRUMErrorEvent.view.referrer, lastRUMViewEvent.view.referrer, "It must include view attributes") + XCTAssertEqual(sendRUMErrorEvent.view.url, lastRUMViewEvent.view.url, "It must include view attributes") XCTAssertNotNil(sendRUMErrorEvent.context, "It must contain context details") XCTAssertNotNil(lastRUMViewEvent.context, "It must contain context details") @@ -690,34 +690,34 @@ class CrashReportReceiverTests: XCTestCase { ) XCTAssertTrue( - sendRUMErrorEvent.model.error.isCrash == true, "The `RUMErrorEvent` sent must be marked as crash." + sendRUMErrorEvent.error.isCrash == true, "The `RUMErrorEvent` sent must be marked as crash." ) XCTAssertEqual( - sendRUMErrorEvent.model.date, + sendRUMErrorEvent.date, crashDate.addingTimeInterval(dateCorrectionOffset).timeIntervalSince1970.toInt64Milliseconds, "The `RUMErrorEvent` sent must include crash date corrected by current correction offset." ) XCTAssertEqual( - sendRUMErrorEvent.model.error.type, + sendRUMErrorEvent.error.type, "SIG_CODE (SIG_NAME)" ) XCTAssertEqual( - sendRUMErrorEvent.model.error.message, + sendRUMErrorEvent.error.message, "Signal details" ) XCTAssertEqual( - sendRUMErrorEvent.model.error.stack, + sendRUMErrorEvent.error.stack, """ 0: stack-trace line 0 1: stack-trace line 1 2: stack-trace line 2 """ ) - XCTAssertEqual(sendRUMErrorEvent.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.threads]), crashReport.threads) - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.binaryImages]), crashReport.binaryImages) - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.meta]), crashReport.meta) - DDAssertJSONEqual(AnyEncodable(sendRUMErrorEvent.additionalAttributes?[DDError.wasTruncated]), crashReport.wasTruncated) + XCTAssertEqual(sendRUMErrorEvent.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + DDAssertJSONEqual(sendRUMErrorEvent.error.threads, crashReport.threads) + DDAssertJSONEqual(sendRUMErrorEvent.error.binaryImages, crashReport.binaryImages) + DDAssertJSONEqual(sendRUMErrorEvent.error.meta, crashReport.meta) + DDAssertJSONEqual(sendRUMErrorEvent.error.wasTruncated, crashReport.wasTruncated) } // MARK: - Testing Uploaded Data - Crashes During RUM Session With No Active View @@ -1000,8 +1000,8 @@ class CrashReportReceiverTests: XCTestCase { let sentRUMView = featureScope.eventsWritten(ofType: RUMViewEvent.self).last XCTAssertEqual(sentRUMView?.view.name, mappedViewName, "Must send mapped RUMView event") - let sentRUMError = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] - XCTAssertEqual(sentRUMError.model.error.fingerprint, errorFingerprint, "Must send mapped RUMError event") + let sentRUMError = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] + XCTAssertEqual(sentRUMError.error.fingerprint, errorFingerprint, "Must send mapped RUMError event") } try test( @@ -1276,10 +1276,10 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(sentRUMView?.view.name, mappedViewName, "Must send mapped RUM view event") // Sent RUM Error should be unmapped - let sentRUMError = featureScope.eventsWritten(ofType: RUMCrashEvent.self)[0] - XCTAssertNil(sentRUMError.model.error.fingerprint) + let sentRUMError = featureScope.eventsWritten(ofType: RUMErrorEvent.self)[0] + XCTAssertNil(sentRUMError.error.fingerprint) DDAssertReflectionEqual( - sentRUMError.model.connectivity, + sentRUMError.connectivity, RUMConnectivity(networkInfo: randomNetworkConnectionInfo, carrierInfo: randomCarrierInfo), "It must contain connectity info from the moment of crash" ) @@ -1288,18 +1288,18 @@ class CrashReportReceiverTests: XCTestCase { RUMUser(userInfo: randomUserInfo), "It must contain user info from the moment of crash" ) - XCTAssertTrue(sentRUMError.model.error.isCrash == true, "RUM error must be marked as crash.") + XCTAssertTrue(sentRUMError.error.isCrash == true, "RUM error must be marked as crash.") XCTAssertEqual( - sentRUMError.model.date, + sentRUMError.date, crashDate.addingTimeInterval(dateCorrectionOffset).timeIntervalSince1970.toInt64Milliseconds, "RUM error must include crash date corrected by current correction offset." ) - XCTAssertEqual(sentRUMError.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.threads], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.binaryImages], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.meta], "It must contain crash details") - XCTAssertNotNil(sentRUMError.additionalAttributes?[DDError.wasTruncated], "It must contain crash details") - XCTAssertEqual(sentRUMError.model.error.sourceType, .ios, "Must send .ios as the sourceType") + XCTAssertEqual(sentRUMError.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") + XCTAssertNotNil(sentRUMError.error.threads, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.binaryImages, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.meta, "It must contain crash details") + XCTAssertNotNil(sentRUMError.error.wasTruncated, "It must contain crash details") + XCTAssertEqual(sentRUMError.error.sourceType, .ios, "Must send .ios as the sourceType") } try test(