From df5b39fde97b2797ee8c885680263024e152c73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barbora=20Pla=C5=A1ovsk=C3=A1?= Date: Wed, 12 Aug 2026 10:05:42 +0200 Subject: [PATCH 1/6] RUM-18007 Sanitize vital and fatal-error RUM events before writing --- Datadog/Datadog.xcodeproj/project.pbxproj | 4 + .../CrashReportReceiverTests.swift | 84 +++++++++++++++++++ .../WatchdogTerminationReporter.swift | 3 +- .../Integrations/CrashReportReceiver.swift | 9 +- .../Sources/RUMEvent/RUMEventSanitizer.swift | 4 + .../Scopes/RUMAppLaunchManager.swift | 6 +- .../Scopes/RUMFeatureOperationManager.swift | 3 +- .../WatchdogTerminationReporterTests.swift | 45 ++++++++++ .../RUMEvent/RUMEventSanitizerTests.swift | 6 ++ .../RUMFeatureOperationManagerTests.swift | 16 ++++ .../Scopes/RUMAppLaunchManagerTests.swift | 19 +++++ .../DatadogInternal/RUMDataModelMocks.swift | 12 +++ 12 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 1884b1e9a4..1d8b4800f0 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -278,6 +278,7 @@ 3C3EF2B02C1AEBAB009E9E57 /* LaunchReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */; }; 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; + B21F9B173E636896FBBF638B /* WatchdogTerminationReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11A0F1FDC6F852757B99EF56 /* WatchdogTerminationReporterTests.swift */; }; 3C4CF9922C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */; }; 3C4CF9982C47CC91006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */; }; 3C4CF99B2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */; }; @@ -2113,6 +2114,7 @@ 3C3C9E2E2C64F470003AF22F /* Data+CryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+CryptoTests.swift"; sourceTree = ""; }; 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchReport.swift; sourceTree = ""; }; 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitorTests.swift; sourceTree = ""; }; + 11A0F1FDC6F852757B99EF56 /* WatchdogTerminationReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationReporterTests.swift; sourceTree = ""; }; 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitorTests.swift; sourceTree = ""; }; 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMocks.swift; sourceTree = ""; }; 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitor.swift; sourceTree = ""; }; @@ -3957,6 +3959,7 @@ 3CFF4FA32C0E0FE5006F191D /* WatchdogTerminationCheckerTests.swift */, 3CEC57752C16FDD30042B5F2 /* AppStateManagerTests.swift */, 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */, + 11A0F1FDC6F852757B99EF56 /* WatchdogTerminationReporterTests.swift */, ); path = WatchdogTerminations; sourceTree = ""; @@ -9463,6 +9466,7 @@ 3C4CF99B2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */, 5B1D02942E8ED6C000AB2391 /* FlagEvaluationReceiverTests.swift in Sources */, 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, + B21F9B173E636896FBBF638B /* WatchdogTerminationReporterTests.swift in Sources */, D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */, 6174D61A2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, 9654971D2D774060006428EE /* SwiftUIViewNameExtractorTests.swift in Sources */, diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index ca7188bc43..689422b6fa 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -154,6 +154,90 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(featureScope.eventsWritten(ofType: RUMErrorEvent.self).count, 1) } + func testGivenCrashDuringRUMSessionWithActiveViewCollectedLessThan4HoursAgoAndTooManyAttributes_whenSending_itSanitizesRUMErrorContext() throws { + let secondsIn4Hours: TimeInterval = 4 * 60 * 60 + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + + // Given + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let crashDate: Date = currentDate.secondsAgo(.random(in: 0.. Bool { diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift new file mode 100644 index 0000000000..edeaccce30 --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift @@ -0,0 +1,45 @@ +/* + * 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-2020 Datadog, Inc. + */ + +import XCTest +import DatadogInternal +@testable import DatadogRUM +import TestUtilities + +final class WatchdogTerminationReporterTests: XCTestCase { + let featureScope = FeatureScopeMock() + + func testSend_sanitizesRUMErrorContextBeforeWriting() throws { + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + + // Given + var viewEvent: RUMViewEvent = .mockRandomWith(crashCount: 0) + viewEvent.context = RUMEventAttributes( + contextInfo: Dictionary(uniqueKeysWithValues: (0..(event: Event) where Event: RUMSanitizableEvent { @@ -84,6 +86,8 @@ class RUMEventSanitizerTests: XCTestCase { test(event: actionEvent) test(event: errorEvent) test(event: longTaskEvent) + test(event: vitalAppLaunchEvent) + test(event: vitalOperationStepEvent) } func testWhenNumberOfAttributesExceedsLimit_itDropsExtraOnes() { @@ -125,6 +129,8 @@ class RUMEventSanitizerTests: XCTestCase { test(event: actionEvent) test(event: errorEvent) test(event: longTaskEvent) + test(event: vitalAppLaunchEvent) + test(event: vitalOperationStepEvent) } // MARK: - Private diff --git a/DatadogRUM/Tests/RUMMonitor/RUMFeatureOperationManagerTests.swift b/DatadogRUM/Tests/RUMMonitor/RUMFeatureOperationManagerTests.swift index 76d60ea361..524b8cf622 100644 --- a/DatadogRUM/Tests/RUMMonitor/RUMFeatureOperationManagerTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/RUMFeatureOperationManagerTests.swift @@ -98,6 +98,22 @@ class RUMFeatureOperationManagerTests: XCTestCase { XCTAssertEqual(event.dd.profiling?.quotaReason, quotaReason) } + func testFeatureOperationCommand_sanitizesContextAttributesBeforeWriting() throws { + // Given + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + let attributes = Dictionary( + uniqueKeysWithValues: (0.. RUMVitalOperationStepEvent { return RUMVitalOperationStepEvent( dd: .init(), + account: .mockRandom(), application: .init(id: .mockRandom()), + context: .mockRandom(), date: .mockRandom(), session: .init(id: .mockRandom(), type: .user), + usr: .mockRandom(), view: .init(id: .mockRandom(), url: .mockRandom()), vital: .mockRandom() ) @@ -786,9 +789,12 @@ extension RUMVitalAppLaunchEvent: RandomMockable, AnyMockable { public static func mockRandom() -> Self { return RUMVitalAppLaunchEvent( dd: .init(), + account: .mockRandom(), application: .init(id: .mockRandom()), + context: .mockRandom(), date: .mockRandom(), session: .init(id: .mockRandom(), type: .user), + usr: .mockRandom(), view: .init(id: .mockRandom(), url: .mockRandom()), vital: .mockRandom() ) @@ -798,17 +804,23 @@ extension RUMVitalAppLaunchEvent: RandomMockable, AnyMockable { public static func mockWith( dd: DD = .init(), + account: RUMAccount? = .mockRandom(), application: Application = .init(id: .mockAny()), + context: RUMEventAttributes? = .mockRandom(), date: Int64 = .mockAny(), session: Session = .init(id: .mockAny(), type: .user), + usr: RUMUser? = .mockRandom(), view: View = .init(id: .mockAny(), url: .mockAny()), vital: Vital = .mockAny() ) -> Self { .init( dd: dd, + account: account, application: application, + context: context, date: date, session: session, + usr: usr, view: view, vital: vital ) From 2e7ad38c0f64937b67b439b3a62a9280af103751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barbora=20Pla=C5=A1ovsk=C3=A1?= Date: Thu, 13 Aug 2026 09:40:23 +0200 Subject: [PATCH 2/6] RUM-18007 Sanitize paired view updates in crash and watchdog reporting --- .../CrashReportReceiverTests.swift | 42 +++++++++++++++++++ .../WatchdogTerminationReporter.swift | 2 +- .../Integrations/CrashReportReceiver.swift | 3 +- .../WatchdogTerminationReporterTests.swift | 31 ++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index 689422b6fa..970d06dd3e 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -238,6 +238,48 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(contextInfoCount, AttributesSanitizer.Constraints.maxNumberOfAttributes - usrInfoCount - accountInfoCount, "`contextInfo` is removed first, then `account`, when the total exceeds the limit") } + func testGivenCrashDuringRUMSessionWithActiveViewCollectedLessThan4HoursAgoAndTooManyAttributes_whenSending_itSanitizesRUMViewContext() throws { + let secondsIn4Hours: TimeInterval = 4 * 60 * 60 + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + + // Given + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let crashDate: Date = currentDate.secondsAgo(.random(in: 0.. Date: Thu, 13 Aug 2026 10:08:24 +0200 Subject: [PATCH 3/6] RUM-18007 Sanitize mapped crash view before writing --- .../CrashReportReceiverTests.swift | 43 +++++++++++++++++++ .../Integrations/CrashReportReceiver.swift | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index 970d06dd3e..52846e6bf1 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -280,6 +280,49 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(contextInfoCount, AttributesSanitizer.Constraints.maxNumberOfAttributes - usrInfoCount - accountInfoCount, "`contextInfo` is removed first, then `account`, when the total exceeds the limit") } + func testGivenCrashDuringRUMSessionWithActiveViewAndViewEventMapperReintroducingTooManyAttributes_whenSending_itSanitizesTheMappedRUMViewContext() throws { + let secondsIn4Hours: TimeInterval = 4 * 60 * 60 + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + + // Given + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let crashDate: Date = currentDate.secondsAgo(.random(in: 0.. Date: Thu, 13 Aug 2026 11:14:46 +0200 Subject: [PATCH 4/6] RUM-18007 Map raw crash view before sanitizing in CrashReportReceiver --- .../CrashReportReceiverTests.swift | 44 +++++++++++++++++++ .../Integrations/CrashReportReceiver.swift | 3 +- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift index 52846e6bf1..768a2ee4b0 100644 --- a/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/Integrations/CrashReportReceiverTests.swift @@ -323,6 +323,50 @@ class CrashReportReceiverTests: XCTestCase { XCTAssertEqual(usrInfoCount + accountInfoCount + contextInfoCount, AttributesSanitizer.Constraints.maxNumberOfAttributes, "the view returned by `viewEventMapper` must be sanitized before writing") } + func testGivenCrashDuringRUMSessionWithActiveView_whenSending_itMapsTheRawRUMViewContextBeforeSanitizing() throws { + let secondsIn4Hours: TimeInterval = 4 * 60 * 60 + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + + // Given + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let crashDate: Date = currentDate.secondsAgo(.random(in: 0.. Date: Thu, 13 Aug 2026 11:15:08 +0200 Subject: [PATCH 5/6] RUM-18007 Pin dates in WatchdogTerminationReporterTests to fix flakiness --- .../WatchdogTerminationReporterTests.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift index 5eaf92d321..51214fb728 100644 --- a/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift +++ b/DatadogRUM/Tests/Instrumentation/WatchdogTerminations/WatchdogTerminationReporterTests.swift @@ -14,6 +14,7 @@ final class WatchdogTerminationReporterTests: XCTestCase { func testSend_sanitizesRUMErrorContextBeforeWriting() throws { let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + let currentDate: Date = .mockDecember15th2019At10AMUTC() // Given var viewEvent: RUMViewEvent = .mockRandomWith(crashCount: 0) @@ -23,13 +24,13 @@ final class WatchdogTerminationReporterTests: XCTestCase { let reporter = WatchdogTerminationReporter( featureScope: featureScope, - dateProvider: DateProviderMock(), + dateProvider: RelativeDateProvider(using: currentDate), uuidGenerator: RUMUUIDGeneratorMock() ) // When reporter.send( - date: Date(timeIntervalSinceReferenceDate: TimeInterval(viewEvent.date)), + date: currentDate, state: .mockWith(trackingConsent: .granted), viewEvent: viewEvent ) @@ -45,6 +46,7 @@ final class WatchdogTerminationReporterTests: XCTestCase { func testSend_sanitizesRUMViewContextBeforeWriting() throws { let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + let currentDate: Date = .mockDecember15th2019At10AMUTC() // Given var viewEvent: RUMViewEvent = .mockRandomWith(crashCount: 0) @@ -54,13 +56,13 @@ final class WatchdogTerminationReporterTests: XCTestCase { let reporter = WatchdogTerminationReporter( featureScope: featureScope, - dateProvider: DateProviderMock(), + dateProvider: RelativeDateProvider(using: currentDate), uuidGenerator: RUMUUIDGeneratorMock() ) // When reporter.send( - date: Date(timeIntervalSinceReferenceDate: TimeInterval(viewEvent.date)), + date: currentDate, state: .mockWith(trackingConsent: .granted), viewEvent: viewEvent ) From 6b2b817a2237ac6a353185732a314862665048cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barbora=20Pla=C5=A1ovsk=C3=A1?= Date: Thu, 13 Aug 2026 14:04:45 +0200 Subject: [PATCH 6/6] RUM-18007 Sanitize fatal app hang error and view events before writing --- .../AppHangs/FatalAppHangsHandler.swift | 5 +- .../AppHangs/AppHangsMonitorTests.swift | 74 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift index 34b9fbc02c..2dee35f7c2 100644 --- a/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/AppHangs/FatalAppHangsHandler.swift @@ -17,6 +17,7 @@ internal final class FatalAppHangsHandler { /// Device date provider. private let dateProvider: DateProvider private let uuidGenerator: RUMUUIDGenerator + private let sanitizer = RUMEventSanitizer() init( featureScope: FeatureScope, @@ -117,8 +118,8 @@ internal final class FatalAppHangsHandler { additionalAttributes: nil, timeSinceAppStart: timeSinceAppStart ) - let error = builder.createRUMError(with: fatalHang.lastRUMView) - let view = builder.updateRUMViewWithError(fatalHang.lastRUMView) + let error = self.sanitizer.sanitize(event: builder.createRUMError(with: fatalHang.lastRUMView)) + let view = self.sanitizer.sanitize(event: builder.updateRUMViewWithError(fatalHang.lastRUMView)) 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 19f68d62cc..fac42873d0 100644 --- a/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift +++ b/DatadogRUM/Tests/Instrumentation/AppHangs/AppHangsMonitorTests.swift @@ -282,6 +282,80 @@ class AppHangsMonitorTests: XCTestCase { XCTAssertNil(featureScope.dataStoreMock.value(forKey: RUMDataStore.Key.fatalAppHangKey.rawValue)) } + func testGivenPendingHangWithTooManyAttributes_whenStartedInAnotherProcess_itSanitizesRUMErrorContextBeforeWriting() throws { + let numberOfAttributes = AttributesSanitizer.Constraints.maxNumberOfAttributes * 2 + let currentDate: Date = .mockDecember15th2019At10AMUTC() + let hangDate: Date = currentDate.secondsAgo(.random(in: 0...4.hours)) + var view: RUMViewEvent = .mockRandom() + view.context = RUMEventAttributes( + contextInfo: Dictionary(uniqueKeysWithValues: (0..