From 732291405f014fc686d6b8f471ad60e1911a5e32 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 28 Oct 2024 13:34:01 +0100 Subject: [PATCH 01/38] chore: Temporarily disable CI Tests as causing test failures was causing: """ Failed to delete `TestsDirectory`: Error Domain=NSCocoaErrorDomain Code=512 ... couldn't be removed." """ --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9205d662c5..96d9aeaa21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -103,7 +103,7 @@ Unit Tests (iOS): script: - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" - make clean repo-setup ENV=ci - - make test-ios-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=1 + - make test-ios-all OS="$DEFAULT_IOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=0 Unit Tests (tvOS): stage: test @@ -116,7 +116,7 @@ Unit Tests (tvOS): script: - ./tools/runner-setup.sh --xcode "$DEFAULT_XCODE" - make clean repo-setup ENV=ci - - make test-tvos-all OS="$DEFAULT_TVOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=1 + - make test-tvos-all OS="$DEFAULT_TVOS_OS" PLATFORM="$PLATFORM" DEVICE="$DEVICE" USE_TEST_VISIBILITY=0 UI Tests: stage: ui-test From 2566ddd98a6b5723505f03d1b6fd17f46f1a0814 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Mon, 28 Oct 2024 13:42:31 +0100 Subject: [PATCH 02/38] Update api-surface-swift --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- api-surface-swift | 36 ++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 744ed25504..2f187a2865 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,4 +10,4 @@ A brief description of implementation details of this PR. - [ ] Feature or bugfix MUST have appropriate tests (unit, integration) - [ ] Make sure each commit and the PR mention the Issue number or JIRA reference - [ ] Add CHANGELOG entry for user facing changes -- [ ] Add Objective-C interface for public APIs (see our [guidelines](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3157787243/RFC+-+Modular+Objective-C+Interface#Recommended-solution) (internal)) and run `make api-surface` +- [ ] Add Objective-C interface for public APIs (see our [guidelines](https://datadoghq.atlassian.net/wiki/spaces/RUMP/pages/3157787243/RFC+-+Modular+Objective-C+Interface#Recommended-solution) [internal]) and run `make api-surface`) diff --git a/api-surface-swift b/api-surface-swift index ad45de4623..976481b0d8 100644 --- a/api-surface-swift +++ b/api-surface-swift @@ -2200,6 +2200,38 @@ public enum SessionReplay public var touchPrivacyLevel: TouchPrivacyLevel public var startRecordingImmediately: Bool public var customEndpoint: URL? - public init(replaySampleRate: Float,textAndInputPrivacyLevel: TextAndInputPrivacyLevel,imagePrivacyLevel: ImagePrivacyLevel,touchPrivacyLevel: TouchPrivacyLevel,startRecordingImmediately: Bool = true,customEndpoint: URL? = nil) - public init(replaySampleRate: Float,defaultPrivacyLevel: SessionReplayPrivacyLevel = .mask,startRecordingImmediately: Bool = true,customEndpoint: URL? = nil) + public init( // swiftlint:disable:this function_default_parameter_at_endreplaySampleRate: Float = 100,textAndInputPrivacyLevel: TextAndInputPrivacyLevel,imagePrivacyLevel: ImagePrivacyLevel,touchPrivacyLevel: TouchPrivacyLevel,startRecordingImmediately: Bool = true,customEndpoint: URL? = nil) + public init(replaySampleRate: Float = 100,defaultPrivacyLevel: SessionReplayPrivacyLevel = .mask,startRecordingImmediately: Bool = true,customEndpoint: URL? = nil) public mutating func setAdditionalNodeRecorders(_ additionalNodeRecorders: [SessionReplayNodeRecorder]) +public enum objc_TextAndInputPrivacyLevelOverride: Int + case none + case maskSensitiveInputs + case maskAllInputs + case maskAll +public enum objc_ImagePrivacyLevelOverride: Int + case none + case maskNone + case maskNonBundledOnly + case maskAll +public enum objc_TouchPrivacyLevelOverride: Int + case none + case show + case hide +[?] extension DatadogExtension where ExtendedType: UIView + public var sessionReplayPrivacyOverrides: SessionReplayPrivacyOverrides +public final class SessionReplayPrivacyOverrides + public init(_ view: UIView) + public var textAndInputPrivacy: TextAndInputPrivacyLevel? + public var imagePrivacy: ImagePrivacyLevel? + public var touchPrivacy: TouchPrivacyLevel? + public var hide: Bool? +[?] extension PrivacyOverrides: Equatable + public static func == (lhs: SessionReplayPrivacyOverrides, rhs: SessionReplayPrivacyOverrides) -> Bool +public extension UIView + @objc var ddSessionReplayPrivacyOverrides: objc_SessionReplayPrivacyOverrides +public final class objc_SessionReplayPrivacyOverrides: NSObject + public init(view: UIView) + @objc public var textAndInputPrivacy: objc_TextAndInputPrivacyLevelOverride + @objc public var imagePrivacy: objc_ImagePrivacyLevelOverride + @objc public var touchPrivacy: objc_TouchPrivacyLevelOverride + @objc public var hide: NSNumber? From ae347887fcc13faad3f7c9e39b09dc8250b49087 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 22 Oct 2024 14:14:09 +0200 Subject: [PATCH 03/38] RUM-6698 chore: Inject NotificationCenter as dependency this is to enable integration testing through simulating lifecycle events --- .../Core/Context/ApplicationStatePublisher.swift | 16 ++++++++-------- .../Core/Context/BatteryStatusPublisher.swift | 6 +++--- .../Core/Context/LowPowerModePublisher.swift | 6 +++--- DatadogCore/Sources/Core/DatadogCore.swift | 12 ++++++++---- DatadogCore/Sources/Datadog.swift | 6 +++++- .../Context/ApplicationStatePublisherTests.swift | 8 ++++---- .../Context/BatteryStatusPublisherTests.swift | 4 ++-- .../Context/LowPowerModePublisherTests.swift | 4 ++-- .../RUM/RUMVitals/VitalInfoSamplerTests.swift | 4 ++-- DatadogRUM/Sources/Feature/RUMFeature.swift | 4 +++- .../MemoryWarnings/MemoryWarningMonitor.swift | 2 +- .../Instrumentation/RUMInstrumentation.swift | 7 ++++++- .../Instrumentation/Views/RUMViewsHandler.swift | 2 +- DatadogRUM/Sources/RUMConfiguration.swift | 2 ++ .../RUMMonitor/Scopes/RUMScopeDependencies.swift | 4 ++-- .../Sources/RUMVitals/VitalCPUReader.swift | 2 +- .../RUMVitals/VitalRefreshRateReader.swift | 2 +- .../RUMInstrumentationTests.swift | 7 +++++++ .../Views/RUMViewsHandlerTests.swift | 10 +++++----- 19 files changed, 66 insertions(+), 42 deletions(-) diff --git a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift index e6b1096797..df11d4be08 100644 --- a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift +++ b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift @@ -60,14 +60,14 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// /// - Parameters: /// - initialState: The initial application state. + /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. /// - queue: The queue for publishing the history. /// - dateProvider: The date provider for the Application state snapshot timestamp. - /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. init( initialState: AppState, + notificationCenter: NotificationCenter, queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider(), - notificationCenter: NotificationCenter = .default + dateProvider: DateProvider = SystemDateProvider() ) { let initialValue = AppStateHistory( initialState: initialState, @@ -87,21 +87,21 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// **Note**: It must be called on the main thread. /// /// - Parameters: + /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. /// - applicationState: The current shared `UIApplication` state. /// - queue: The queue for publishing the history. /// - dateProvider: The date provider for the Application state snapshot timestamp. - /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. convenience init( + notificationCenter: NotificationCenter, applicationState: ApplicationState = ApplicationStatePublisher.currentApplicationState, queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider(), - notificationCenter: NotificationCenter = .default + dateProvider: DateProvider = SystemDateProvider() ) { self.init( initialState: AppState(applicationState), + notificationCenter: notificationCenter, queue: queue, - dateProvider: dateProvider, - notificationCenter: notificationCenter + dateProvider: dateProvider ) } diff --git a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift index db33ee23b5..041c8a49e6 100644 --- a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift +++ b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift @@ -24,11 +24,11 @@ internal final class BatteryStatusPublisher: ContextValuePublisher { /// Creates a battery status publisher from the given device. /// /// - Parameters: - /// - device: The `UIDevice` instance. `.current` by default. /// - notificationCenter: The notification center for observing the `UIDevice` battery changes, + /// - device: The `UIDevice` instance. `.current` by default. init( - device: UIDevice = .current, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter, + device: UIDevice = .current ) { self.device = device self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift index 11168138a3..b62027f207 100644 --- a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift +++ b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift @@ -19,11 +19,11 @@ internal final class LowPowerModePublisher: ContextValuePublisher { /// Creates a low power mode publisher. /// /// - Parameters: - /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. /// - notificationCenter: The notification center for observing the `NSProcessInfoPowerStateDidChange`, + /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. init( - processInfo: ProcessInfo = .processInfo, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter, + processInfo: ProcessInfo = .processInfo ) { self.initialValue = processInfo.isLowPowerModeEnabled self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index e04d7e2450..afd02534ef 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -401,7 +401,8 @@ extension DatadogContextProvider { sdkInitDate: Date, device: DeviceInfo, dateProvider: DateProvider, - serverDateProvider: ServerDateProvider + serverDateProvider: ServerDateProvider, + notificationCenter: NotificationCenter ) { let context = DatadogContext( site: site, @@ -442,14 +443,17 @@ extension DatadogContextProvider { #endif #if os(iOS) && !targetEnvironment(simulator) - subscribe(\.batteryStatus, to: BatteryStatusPublisher()) - subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher()) + subscribe(\.batteryStatus, to: BatteryStatusPublisher(notificationCenter: notificationCenter)) + subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher(notificationCenter: notificationCenter)) #endif #if os(iOS) || os(tvOS) DispatchQueue.main.async { // must be call on the main thread to read `UIApplication.State` - let applicationStatePublisher = ApplicationStatePublisher(dateProvider: dateProvider) + let applicationStatePublisher = ApplicationStatePublisher( + notificationCenter: notificationCenter, + dateProvider: dateProvider + ) self.subscribe(\.applicationStateHistory, to: applicationStatePublisher) } #endif diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 5e3a487ce1..ea825e18e0 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -226,6 +226,9 @@ public enum Datadog { internal var httpClientFactory: ([AnyHashable: Any]?) -> HTTPClient = { proxyConfiguration in URLSessionClient(proxyConfiguration: proxyConfiguration) } + + /// The default notification center used for subscribing to app lifecycle events and system notifications. + internal var notificationCenter: NotificationCenter = .default } /// Verbosity level of Datadog SDK. Can be used for debugging purposes. @@ -466,7 +469,8 @@ public enum Datadog { sdkInitDate: configuration.dateProvider.now, device: DeviceInfo(), dateProvider: configuration.dateProvider, - serverDateProvider: configuration.serverDateProvider + serverDateProvider: configuration.serverDateProvider, + notificationCenter: configuration.notificationCenter ), applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift index 26c463970a..442430d0e0 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift @@ -27,8 +27,8 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( initialState: .mockRandom(), - dateProvider: SystemDateProvider(), - notificationCenter: notificationCenter + notificationCenter: notificationCenter, + dateProvider: SystemDateProvider() ) // When @@ -58,8 +58,8 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( initialState: .mockRandom(), - dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0), - notificationCenter: notificationCenter + notificationCenter: notificationCenter, + dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0) ) var receivedHistoryStates: [AppState?] = [] diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift index fdd07afa3f..21465677f1 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/BatteryStatusPublisherTests.swift @@ -19,7 +19,7 @@ final class BatteryStatusPublisherTests: XCTestCase { // Given let device = UIDeviceMock(batteryState: .unknown) - let publisher = BatteryStatusPublisher(device: device, notificationCenter: notificationCenter) + let publisher = BatteryStatusPublisher(notificationCenter: notificationCenter, device: device) publisher.publish { status in // Then XCTAssertEqual(status?.state, .charging) @@ -38,7 +38,7 @@ final class BatteryStatusPublisherTests: XCTestCase { // Given let device = UIDeviceMock(batteryLevel: 0.5) - let publisher = BatteryStatusPublisher(device: device, notificationCenter: notificationCenter) + let publisher = BatteryStatusPublisher(notificationCenter: notificationCenter, device: device) publisher.publish { status in // Then XCTAssertEqual(status?.level, 0.75) diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift index 1eb2389e7e..7e79241b5c 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/LowPowerModePublisherTests.swift @@ -17,8 +17,8 @@ class LowPowerModePublisherTests: XCTestCase { // Given let isLowPowerModeEnabled: Bool = .random() let publisher = LowPowerModePublisher( - processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled), - notificationCenter: notificationCenter + notificationCenter: notificationCenter, + processInfo: ProcessInfoMock(isLowPowerModeEnabled: isLowPowerModeEnabled) ) XCTAssertEqual(publisher.initialValue, isLowPowerModeEnabled) diff --git a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift index 26df699b63..aaafdfc184 100644 --- a/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift +++ b/DatadogCore/Tests/Datadog/RUM/RUMVitals/VitalInfoSamplerTests.swift @@ -99,9 +99,9 @@ class VitalInfoSamplerTests: XCTestCase { DispatchQueue.global().sync { // in real-world scenarios, sampling will be started from background threads sampler = VitalInfoSampler( - cpuReader: VitalCPUReader(), + cpuReader: VitalCPUReader(notificationCenter: .default), memoryReader: VitalMemoryReader(), - refreshRateReader: VitalRefreshRateReader(), + refreshRateReader: VitalRefreshRateReader(notificationCenter: .default), frequency: 0.1 ) } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index 24edb64a6a..8f0d805192 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -114,7 +114,8 @@ internal final class RUMFeature: DatadogRemoteFeature { let memoryWarningReporter = MemoryWarningReporter() let memoryWarningMonitor = MemoryWarningMonitor( backtraceReporter: core.backtraceReporter, - memoryWarningReporter: memoryWarningReporter + memoryWarningReporter: memoryWarningReporter, + notificationCenter: configuration.notificationCenter ) self.instrumentation = RUMInstrumentation( @@ -128,6 +129,7 @@ internal final class RUMFeature: DatadogRemoteFeature { backtraceReporter: core.backtraceReporter, fatalErrorContext: dependencies.fatalErrorContext, processID: configuration.processID, + notificationCenter: configuration.notificationCenter, watchdogTermination: watchdogTermination, memoryWarningMonitor: memoryWarningMonitor ) diff --git a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift index 06aa9ac492..1f3c77a70d 100644 --- a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift +++ b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift @@ -17,7 +17,7 @@ internal final class MemoryWarningMonitor { init( backtraceReporter: BacktraceReporting?, memoryWarningReporter: MemoryWarningReporting, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter ) { self.notificationCenter = notificationCenter self.backtraceReporter = backtraceReporter diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 3324111a3f..8eb800bedf 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -55,12 +55,17 @@ internal final class RUMInstrumentation: RUMCommandPublisher { backtraceReporter: BacktraceReporting, fatalErrorContext: FatalErrorContextNotifying, processID: UUID, + notificationCenter: NotificationCenter, watchdogTermination: WatchdogTerminationMonitor?, memoryWarningMonitor: MemoryWarningMonitor ) { // Always create views handler (we can't know if it will be used by SwiftUI instrumentation) // and only swizzle `UIViewController` if UIKit instrumentation is configured: - let viewsHandler = RUMViewsHandler(dateProvider: dateProvider, predicate: uiKitRUMViewsPredicate) + let viewsHandler = RUMViewsHandler( + dateProvider: dateProvider, + predicate: uiKitRUMViewsPredicate, + notificationCenter: notificationCenter + ) let viewControllerSwizzler: UIViewControllerSwizzler? = { do { if uiKitRUMViewsPredicate != nil { diff --git a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift index 6de74549ea..01ad47bb74 100644 --- a/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift @@ -65,7 +65,7 @@ internal final class RUMViewsHandler { init( dateProvider: DateProvider, predicate: UIKitRUMViewsPredicate?, - notificationCenter: NotificationCenter = .default + notificationCenter: NotificationCenter ) { self.dateProvider = dateProvider self.predicate = predicate diff --git a/DatadogRUM/Sources/RUMConfiguration.swift b/DatadogRUM/Sources/RUMConfiguration.swift index e192539f50..79f1f70f70 100644 --- a/DatadogRUM/Sources/RUMConfiguration.swift +++ b/DatadogRUM/Sources/RUMConfiguration.swift @@ -279,6 +279,8 @@ extension RUM { internal var mainQueue: DispatchQueue = .main /// Identifier of the current process, used to check if fatal App Hang originated in a previous process instance. internal var processID: UUID = currentProcessID + /// The default notification center used for subscribing to app lifecycle events and system notifications. + internal var notificationCenter: NotificationCenter = .default internal var debugSDK: Bool = ProcessInfo.processInfo.arguments.contains(LaunchArguments.Debug) internal var debugViews: Bool = ProcessInfo.processInfo.arguments.contains("DD_DEBUG_RUM") diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift index 807b735908..68449d2d5e 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift @@ -19,9 +19,9 @@ internal struct VitalsReaders { telemetry: Telemetry = NOPTelemetry() ) { self.frequency = frequency - self.cpu = VitalCPUReader(telemetry: telemetry) + self.cpu = VitalCPUReader(notificationCenter: .default, telemetry: telemetry) self.memory = VitalMemoryReader() - self.refreshRate = VitalRefreshRateReader() + self.refreshRate = VitalRefreshRateReader(notificationCenter: .default) } } diff --git a/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift b/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift index b326bea350..76e89ecd8d 100644 --- a/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift +++ b/DatadogRUM/Sources/RUMVitals/VitalCPUReader.swift @@ -18,7 +18,7 @@ internal class VitalCPUReader: SamplingBasedVitalReader { private let telemetry: Telemetry init( - notificationCenter: NotificationCenter = .default, + notificationCenter: NotificationCenter, telemetry: Telemetry = NOPTelemetry() ) { self.telemetry = telemetry diff --git a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift index 900ca922ee..67287a1054 100644 --- a/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift +++ b/DatadogRUM/Sources/RUMVitals/VitalRefreshRateReader.swift @@ -17,7 +17,7 @@ internal class VitalRefreshRateReader: ContinuousVitalReader { private var nextFrameDuration: CFTimeInterval? private let notificationCenter: NotificationCenter - init(notificationCenter: NotificationCenter = .default) { + init(notificationCenter: NotificationCenter) { self.notificationCenter = notificationCenter notificationCenter.addObserver( diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 844c6a32c8..384c03147f 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -25,6 +25,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -52,6 +53,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -76,6 +78,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -103,6 +106,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -126,6 +130,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -149,6 +154,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) @@ -172,6 +178,7 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), + notificationCenter: .default, watchdogTermination: .mockRandom(), memoryWarningMonitor: .mockRandom() ) diff --git a/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift b/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift index f40a875664..0bbfe09ea1 100644 --- a/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift +++ b/DatadogRUM/Tests/Instrumentation/Views/RUMViewsHandlerTests.swift @@ -221,7 +221,7 @@ class RUMViewsHandlerTests: XCTestCase { } } let predicate = Predicate() - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) // Given let someView = createMockViewInWindow() @@ -257,7 +257,7 @@ class RUMViewsHandlerTests: XCTestCase { let untrackedModal = createMockViewInWindow() let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When @@ -292,7 +292,7 @@ class RUMViewsHandlerTests: XCTestCase { let untrackedModal = createMockViewInWindow() let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When @@ -332,7 +332,7 @@ class RUMViewsHandlerTests: XCTestCase { untrackedModal.isModalInPresentation = true let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When @@ -372,7 +372,7 @@ class RUMViewsHandlerTests: XCTestCase { untrackedModal.isModalInPresentation = true let predicate = Predicate(untrackedModal: untrackedModal) - let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate) + let handler = RUMViewsHandler(dateProvider: dateProvider, predicate: predicate, notificationCenter: .default) handler.publish(to: commandSubscriber) // When From 6b62dcadf17856b652ae8561d656565c2b0ecbe5 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 23 Oct 2024 11:26:25 +0200 Subject: [PATCH 04/38] RUM-6698 chore: Enable usage of real DatadogCore initializer in integration tests --- DatadogCore/Sources/Core/DatadogCore.swift | 86 ++++++++++++++++++ DatadogCore/Sources/Datadog.swift | 90 ++----------------- .../DatadogInternal/DatadogCoreProxy.swift | 34 ++++--- 3 files changed, 115 insertions(+), 95 deletions(-) diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index afd02534ef..d2d2a555d4 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -71,6 +71,92 @@ internal final class DatadogCore { /// Maximum number of batches per upload. internal let maxBatchesPerUpload: Int + convenience init( + configuration: Datadog.Configuration, + trackingConsent: TrackingConsent, + instanceName: String + ) throws { + let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) + if debug { + consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) + Datadog.verbosityLevel = .debug + } + + let applicationVersion = configuration.additionalConfiguration[CrossPlatformAttributes.version] as? String + ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + ?? "0.0.0" + + let applicationBuildNumber = configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + ?? "0" + + let bundleName = configuration.bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as? String + let bundleType = BundleType(bundle: configuration.bundle) + let bundleIdentifier = configuration.bundle.bundleIdentifier ?? "unknown" + let service = configuration.service ?? configuration.bundle.bundleIdentifier ?? "ios" + let source = configuration.additionalConfiguration[CrossPlatformAttributes.ddsource] as? String ?? "ios" + let variant = configuration.additionalConfiguration[CrossPlatformAttributes.variant] as? String + let sdkVersion = configuration.additionalConfiguration[CrossPlatformAttributes.sdkVersion] as? String ?? __sdkVersion + let buildId = configuration.additionalConfiguration[CrossPlatformAttributes.buildId] as? String + let nativeSourceType = configuration.additionalConfiguration[CrossPlatformAttributes.nativeSourceType] as? String + + let performance = PerformancePreset( + batchSize: debug ? .small : configuration.batchSize, + uploadFrequency: debug ? .frequent : configuration.uploadFrequency, + bundleType: bundleType + ) + let isRunFromExtension = bundleType == .iOSAppExtension + + self.init( + directory: try CoreDirectory( + in: configuration.systemDirectory(), + instanceName: instanceName, + site: configuration.site + ), + dateProvider: configuration.dateProvider, + initialConsent: trackingConsent, + performance: performance, + httpClient: configuration.httpClientFactory(configuration.proxyConfiguration), + encryption: configuration.encryption, + contextProvider: DatadogContextProvider( + site: configuration.site, + clientToken: configuration.clientToken, + service: service, + env: configuration.env, + version: applicationVersion, + buildNumber: applicationBuildNumber, + buildId: buildId, + variant: variant, + source: source, + nativeSourceOverride: nativeSourceType, + sdkVersion: sdkVersion, + ciAppOrigin: CITestIntegration.active?.origin, + applicationName: bundleName ?? bundleType.rawValue, + applicationBundleIdentifier: bundleIdentifier, + applicationBundleType: bundleType, + applicationVersion: applicationVersion, + sdkInitDate: configuration.dateProvider.now, + device: DeviceInfo(), + dateProvider: configuration.dateProvider, + serverDateProvider: configuration.serverDateProvider, + notificationCenter: configuration.notificationCenter + ), + applicationVersion: applicationVersion, + maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + isRunFromExtension: isRunFromExtension + ) + + telemetry.configuration( + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), + batchSize: performance.uploaderWindow.toInt64Milliseconds, + batchUploadFrequency: performance.minUploadDelay.toInt64Milliseconds, + useLocalEncryption: configuration.encryption != nil, + useProxy: configuration.proxyConfiguration != nil + ) + } + /// Creates a core instance. /// /// - Parameters: diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index ea825e18e0..4959738101 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -406,85 +406,13 @@ public enum Datadog { registerObjcExceptionHandlerOnce() - let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) - if debug { - consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) - Datadog.verbosityLevel = .debug - } - - let applicationVersion = configuration.additionalConfiguration[CrossPlatformAttributes.version] as? String - ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String - ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String - ?? "0.0.0" - - let applicationBuildNumber = configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String - ?? "0" - - let bundleName = configuration.bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as? String - let bundleType = BundleType(bundle: configuration.bundle) - let bundleIdentifier = configuration.bundle.bundleIdentifier ?? "unknown" - let service = configuration.service ?? configuration.bundle.bundleIdentifier ?? "ios" - let source = configuration.additionalConfiguration[CrossPlatformAttributes.ddsource] as? String ?? "ios" - let variant = configuration.additionalConfiguration[CrossPlatformAttributes.variant] as? String - let sdkVersion = configuration.additionalConfiguration[CrossPlatformAttributes.sdkVersion] as? String ?? __sdkVersion - let buildId = configuration.additionalConfiguration[CrossPlatformAttributes.buildId] as? String - let nativeSourceType = configuration.additionalConfiguration[CrossPlatformAttributes.nativeSourceType] as? String - - let performance = PerformancePreset( - batchSize: debug ? .small : configuration.batchSize, - uploadFrequency: debug ? .frequent : configuration.uploadFrequency, - bundleType: bundleType - ) - let isRunFromExtension = bundleType == .iOSAppExtension - - // Set default `DatadogCore`: - let core = DatadogCore( - directory: try CoreDirectory( - in: configuration.systemDirectory(), - instanceName: instanceName, - site: configuration.site - ), - dateProvider: configuration.dateProvider, - initialConsent: trackingConsent, - performance: performance, - httpClient: configuration.httpClientFactory(configuration.proxyConfiguration), - encryption: configuration.encryption, - contextProvider: DatadogContextProvider( - site: configuration.site, - clientToken: try ifValid(clientToken: configuration.clientToken), - service: service, - env: try ifValid(env: configuration.env), - version: applicationVersion, - buildNumber: applicationBuildNumber, - buildId: buildId, - variant: variant, - source: source, - nativeSourceOverride: nativeSourceType, - sdkVersion: sdkVersion, - ciAppOrigin: CITestIntegration.active?.origin, - applicationName: bundleName ?? bundleType.rawValue, - applicationBundleIdentifier: bundleIdentifier, - applicationBundleType: bundleType, - applicationVersion: applicationVersion, - sdkInitDate: configuration.dateProvider.now, - device: DeviceInfo(), - dateProvider: configuration.dateProvider, - serverDateProvider: configuration.serverDateProvider, - notificationCenter: configuration.notificationCenter - ), - applicationVersion: applicationVersion, - maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, - backgroundTasksEnabled: configuration.backgroundTasksEnabled, - isRunFromExtension: isRunFromExtension - ) + try isValid(clientToken: configuration.clientToken) + try isValid(env: configuration.env) - core.telemetry.configuration( - backgroundTasksEnabled: configuration.backgroundTasksEnabled, - batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), - batchSize: performance.uploaderWindow.toInt64Milliseconds, - batchUploadFrequency: performance.minUploadDelay.toInt64Milliseconds, - useLocalEncryption: configuration.encryption != nil, - useProxy: configuration.proxyConfiguration != nil + let core = try DatadogCore( + configuration: configuration, + trackingConsent: trackingConsent, + instanceName: instanceName ) CITestIntegration.active?.startIntegration() @@ -532,7 +460,7 @@ public enum Datadog { } } -private func ifValid(env: String) throws -> String { +private func isValid(env: String) throws { /// 1. cannot be more than 200 chars (including `env:` prefix) /// 2. cannot end with `:` /// 3. can contain letters, numbers and _:./-_ (other chars are converted to _ at backend) @@ -540,12 +468,10 @@ private func ifValid(env: String) throws -> String { if env.range(of: regex, options: .regularExpression, range: nil, locale: nil) == nil { throw ProgrammerError(description: "`env`: \(env) contains illegal characters (only alphanumerics and `_` are allowed)") } - return env } -private func ifValid(clientToken: String) throws -> String { +private func isValid(clientToken: String) throws { if clientToken.isEmpty { throw ProgrammerError(description: "`clientToken` cannot be empty.") } - return clientToken } diff --git a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift index 5e857d5e0c..d0ecde8792 100644 --- a/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift +++ b/DatadogCore/Tests/Datadog/Mocks/DatadogInternal/DatadogCoreProxy.swift @@ -36,20 +36,28 @@ internal class DatadogCoreProxy: DatadogCoreProtocol { @ReadWriteLock private var featureScopeInterceptors: [String: FeatureScopeInterceptor] = [:] - init(context: DatadogContext = .mockAny()) { - self.context = context - self.core = DatadogCore( - directory: temporaryCoreDirectory, - dateProvider: SystemDateProvider(), - initialConsent: context.trackingConsent, - performance: .mockAny(), - httpClient: HTTPClientMock(), - encryption: nil, - contextProvider: DatadogContextProvider(context: context), - applicationVersion: context.version, - maxBatchesPerUpload: .mockRandom(min: 1, max: 100), - backgroundTasksEnabled: .mockAny() + convenience init(context: DatadogContext = .mockAny()) { + self.init( + core: DatadogCore( + directory: temporaryCoreDirectory, + dateProvider: SystemDateProvider(), + initialConsent: context.trackingConsent, + performance: .mockAny(), + httpClient: HTTPClientMock(), + encryption: nil, + contextProvider: DatadogContextProvider( + context: context + ), + applicationVersion: context.version, + maxBatchesPerUpload: .mockRandom(min: 1, max: 100), + backgroundTasksEnabled: .mockAny() + ) ) + } + + init(core: DatadogCore) { + self.context = core.contextProvider.read() + self.core = core // override the message-bus's core instance core.bus.connect(core: self) From d1b304e3f3ae4f1330600888658b8a73591d6282 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 23 Oct 2024 18:17:03 +0200 Subject: [PATCH 05/38] RUM-6698 chore: Inject DateProvider as dependency this is to enable integration testing with simulating elapsing time --- .../Core/Context/ApplicationStatePublisher.swift | 16 ++++++++-------- .../Tests/Datadog/Mocks/RUMFeatureMocks.swift | 4 ++-- DatadogRUM/Sources/Feature/RUMFeature.swift | 2 +- .../RUMMonitor/Scopes/Utils/ViewCache.swift | 2 +- .../Integrations/WebViewEventReceiverTests.swift | 8 ++++---- DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift | 2 +- .../RUMMonitor/Scopes/RUMSessionScopeTests.swift | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift index df11d4be08..7d23042e62 100644 --- a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift +++ b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift @@ -61,13 +61,13 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// - Parameters: /// - initialState: The initial application state. /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. - /// - queue: The queue for publishing the history. /// - dateProvider: The date provider for the Application state snapshot timestamp. + /// - queue: The queue for publishing the history. init( initialState: AppState, notificationCenter: NotificationCenter, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider() + dateProvider: DateProvider, + queue: DispatchQueue = ApplicationStatePublisher.defaultQueue ) { let initialValue = AppStateHistory( initialState: initialState, @@ -88,20 +88,20 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// /// - Parameters: /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. + /// - dateProvider: The date provider for the Application state snapshot timestamp. /// - applicationState: The current shared `UIApplication` state. /// - queue: The queue for publishing the history. - /// - dateProvider: The date provider for the Application state snapshot timestamp. convenience init( notificationCenter: NotificationCenter, + dateProvider: DateProvider, applicationState: ApplicationState = ApplicationStatePublisher.currentApplicationState, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue, - dateProvider: DateProvider = SystemDateProvider() + queue: DispatchQueue = ApplicationStatePublisher.defaultQueue ) { self.init( initialState: AppState(applicationState), notificationCenter: notificationCenter, - queue: queue, - dateProvider: dateProvider + dateProvider: dateProvider, + queue: queue ) } diff --git a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift index ca56405fb8..19451de3cc 100644 --- a/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/RUMFeatureMocks.swift @@ -41,7 +41,7 @@ extension WebViewEventReceiver: AnyMockable { featureScope: FeatureScope = NOPFeatureScope(), dateProvider: DateProvider = SystemDateProvider(), commandSubscriber: RUMCommandSubscriber = RUMCommandSubscriberMock(), - viewCache: ViewCache = ViewCache() + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()) ) -> Self { .init( featureScope: featureScope, @@ -729,7 +729,7 @@ extension RUMScopeDependencies { syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), watchdogTermination: WatchdogTerminationMonitor? = nil diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index 8f0d805192..f99ac5bb14 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -100,7 +100,7 @@ internal final class RUMFeature: DatadogRemoteFeature { ) }, onSessionStart: configuration.onSessionStart, - viewCache: ViewCache(), + viewCache: ViewCache(dateProvider: configuration.dateProvider), fatalErrorContext: FatalErrorContextNotifier(messageBus: featureScope), sessionEndedMetric: sessionEndedMetric, watchdogTermination: watchdogTermination diff --git a/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift b/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift index b002167585..ee72d56d80 100644 --- a/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift +++ b/DatadogRUM/Sources/RUMMonitor/Scopes/Utils/ViewCache.swift @@ -32,7 +32,7 @@ internal final class ViewCache { /// - ttl: The TTL of view ids in cache. /// - capacity: The maximum number of ids to store. init( - dateProvider: DateProvider = SystemDateProvider(), + dateProvider: DateProvider, ttl: TimeInterval = 3.minutes, capacity: Int = 30 ) { diff --git a/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift b/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift index c740f49d7e..abfa1cc8a1 100644 --- a/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift +++ b/DatadogRUM/Tests/Integrations/WebViewEventReceiverTests.swift @@ -195,7 +195,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(now: .mockDecember15th2019At10AMUTC()), commandSubscriber: commandsSubscriberMock, - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When @@ -214,7 +214,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(), commandSubscriber: RUMCommandSubscriberMock(), - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When @@ -290,7 +290,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(), commandSubscriber: RUMCommandSubscriberMock(), - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When @@ -317,7 +317,7 @@ class WebViewEventReceiverTests: XCTestCase { featureScope: featureScope, dateProvider: DateProviderMock(), commandSubscriber: RUMCommandSubscriberMock(), - viewCache: ViewCache() + viewCache: ViewCache(dateProvider: SystemDateProvider()) ) // When diff --git a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift index 5f7bef6676..4c8adfa8da 100644 --- a/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift +++ b/DatadogRUM/Tests/Mocks/RUMFeatureMocks.swift @@ -773,7 +773,7 @@ extension RUMScopeDependencies { syntheticsTest: RUMSyntheticsTest? = nil, vitalsReaders: VitalsReaders? = nil, onSessionStart: @escaping RUM.SessionListener = mockNoOpSessionListener(), - viewCache: ViewCache = ViewCache(), + viewCache: ViewCache = ViewCache(dateProvider: SystemDateProvider()), fatalErrorContext: FatalErrorContextNotifying = FatalErrorContextNotifierMock(), sessionEndedMetric: SessionEndedMetricController = SessionEndedMetricController(telemetry: NOPTelemetry(), sampleRate: 0), watchdogTermination: WatchdogTerminationMonitor = .mockRandom() diff --git a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 2eb6013b39..267c7584c4 100644 --- a/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -132,7 +132,7 @@ class RUMSessionScopeTests: XCTestCase { // Given let dateProvider = RelativeDateProvider() let ttl: TimeInterval = .mockRandom(min: 2, max: 10) - let viewCache = ViewCache(ttl: ttl) + let viewCache = ViewCache(dateProvider: SystemDateProvider(), ttl: ttl) let scope: RUMSessionScope = .mockWith( parent: parent, From 8b2df27fbf17bbb6d7d298bf829da163197ac23f Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 25 Oct 2024 13:22:20 +0200 Subject: [PATCH 06/38] RUM-6698 chore: Inject AppStateProvider as dependency this is to enable integration testing with simulating different app states --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 +++++ .../Context/ApplicationStatePublisher.swift | 40 ++------------- DatadogCore/Sources/Core/DatadogCore.swift | 7 ++- DatadogCore/Sources/Datadog.swift | 3 ++ .../ApplicationStatePublisherTests.swift | 4 +- .../Sources/Context/AppStateProvider.swift | 50 +++++++++++++++++++ TestUtilities/Mocks/AppStateProvider.swift | 27 ++++++++++ 7 files changed, 104 insertions(+), 39 deletions(-) create mode 100644 DatadogInternal/Sources/Context/AppStateProvider.swift create mode 100644 TestUtilities/Mocks/AppStateProvider.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 647122b78a..178d377a0b 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -307,6 +307,10 @@ 61133C702423993200786299 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; 6115299725E3BEF9004F740E /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */; }; 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */; }; + 6117A4E12CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */; }; + 6117A4E22CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */; }; + 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; + 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; 61181CDD2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; 61193AAE2CB54C7300C3CDF5 /* RUMActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */; }; @@ -2374,6 +2378,8 @@ 611529A425E3DD51004F740E /* ValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisher.swift; sourceTree = ""; }; 611529AD25E3E429004F740E /* ValuePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisherTests.swift; sourceTree = ""; }; 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionDelegate+objc.swift"; sourceTree = ""; }; + 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateProvider.swift; sourceTree = ""; }; + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateProvider.swift; sourceTree = ""; }; 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorContextNotifierTests.swift; sourceTree = ""; }; 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMActionsHandler.swift; sourceTree = ""; }; 611F82022563C66100CB9BDB /* UIKitRUMViewsPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsPredicateTests.swift; sourceTree = ""; }; @@ -5919,6 +5925,7 @@ children = ( E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */, D23039B3298D5235001A1FA3 /* AppState.swift */, + 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */, D23039B4298D5235001A1FA3 /* UserInfo.swift */, D23039B5298D5235001A1FA3 /* BatteryStatus.swift */, D23039B6298D5235001A1FA3 /* CarrierInfo.swift */, @@ -6115,6 +6122,7 @@ D2A7840229A536AD003B03BB /* PrintFunctionMock.swift */, D24C9C5129A7BD12002057CF /* SamplerMock.swift */, D24C9C5429A7C5F3002057CF /* DateProvider.swift */, + 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */, D24C9C6629A7CBF0002057CF /* DDErrorMocks.swift */, D2EBEE4729BA17C400B15732 /* NetworkInstrumentationMocks.swift */, 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, @@ -8744,6 +8752,7 @@ D23039E6298D5236001A1FA3 /* Sysctl.swift in Sources */, 614A708E2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, D2160CF429C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, + 6117A4E12CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */, D23039E1298D5236001A1FA3 /* AppState.swift in Sources */, D2DE63532A30A7CA00441A54 /* CoreRegistry.swift in Sources */, E2AA55EA2C32C76A002FEF28 /* WatchKitExtensions.swift in Sources */, @@ -8976,6 +8985,7 @@ D2579556298ABB04008A1BE5 /* FoundationMocks.swift in Sources */, D2579553298ABB04008A1BE5 /* DatadogContextMock.swift in Sources */, 615B0F8E2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */, + 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */, D24C9C6929A7CE06002057CF /* DDErrorMocks.swift in Sources */, 6167E7142B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */, D2579558298ABB04008A1BE5 /* Encoding.swift in Sources */, @@ -9024,6 +9034,7 @@ D2579579298ABB83008A1BE5 /* FoundationMocks.swift in Sources */, D257957A298ABB83008A1BE5 /* DatadogContextMock.swift in Sources */, 615B0F8F2BB33E0400E9ED6C /* DataStoreMock.swift in Sources */, + 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */, D24C9C6A29A7CE06002057CF /* DDErrorMocks.swift in Sources */, 6167E7152B837F0B00C3CA2D /* BacktraceReportingMocks.swift in Sources */, D257957B298ABB83008A1BE5 /* Encoding.swift in Sources */, @@ -9732,6 +9743,7 @@ D2DA236B298D57AA00C6C7E6 /* Sysctl.swift in Sources */, 614A708F2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, D2160CF529C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, + 6117A4E22CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */, D2DA236C298D57AA00C6C7E6 /* AppState.swift in Sources */, D2DE63542A30A7CA00441A54 /* CoreRegistry.swift in Sources */, E2AA55EC2C32C78B002FEF28 /* WatchKitExtensions.swift in Sources */, diff --git a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift index 7d23042e62..34f5530e6b 100644 --- a/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift +++ b/DatadogCore/Sources/Core/Context/ApplicationStatePublisher.swift @@ -14,14 +14,6 @@ import WatchKit internal final class ApplicationStatePublisher: ContextValuePublisher { typealias Snapshot = AppStateHistory.Snapshot - private static var currentApplicationState: ApplicationState { - #if canImport(WatchKit) - WKExtension.dd.shared.applicationState - #else - UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state - #endif - } - /// The default publisher queue. private static let defaultQueue = DispatchQueue( label: "com.datadoghq.app-state-publisher", @@ -58,19 +50,21 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { /// Creates a Application state publisher for publishing application state /// history. /// + /// **Note**: It must be called on the main thread. + /// /// - Parameters: - /// - initialState: The initial application state. + /// - appStateProvider: The provider to access the current application state. /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. /// - dateProvider: The date provider for the Application state snapshot timestamp. /// - queue: The queue for publishing the history. init( - initialState: AppState, + appStateProvider: AppStateProvider, notificationCenter: NotificationCenter, dateProvider: DateProvider, queue: DispatchQueue = ApplicationStatePublisher.defaultQueue ) { let initialValue = AppStateHistory( - initialState: initialState, + initialState: appStateProvider.current, date: dateProvider.now ) @@ -81,30 +75,6 @@ internal final class ApplicationStatePublisher: ContextValuePublisher { self.notificationCenter = notificationCenter } - /// Creates a Application state publisher for publishing application state - /// history. - /// - /// **Note**: It must be called on the main thread. - /// - /// - Parameters: - /// - notificationCenter: The notification center where this publisher observes `UIApplication` notifications. - /// - dateProvider: The date provider for the Application state snapshot timestamp. - /// - applicationState: The current shared `UIApplication` state. - /// - queue: The queue for publishing the history. - convenience init( - notificationCenter: NotificationCenter, - dateProvider: DateProvider, - applicationState: ApplicationState = ApplicationStatePublisher.currentApplicationState, - queue: DispatchQueue = ApplicationStatePublisher.defaultQueue - ) { - self.init( - initialState: AppState(applicationState), - notificationCenter: notificationCenter, - dateProvider: dateProvider, - queue: queue - ) - } - func publish(to receiver: @escaping ContextValueReceiver) { queue.async { self.receiver = receiver } notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive), name: ApplicationNotifications.didBecomeActive, object: nil) diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index d2d2a555d4..0e31f33914 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -139,7 +139,8 @@ internal final class DatadogCore { device: DeviceInfo(), dateProvider: configuration.dateProvider, serverDateProvider: configuration.serverDateProvider, - notificationCenter: configuration.notificationCenter + notificationCenter: configuration.notificationCenter, + appStateProvider: configuration.appStateProvider ), applicationVersion: applicationVersion, maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, @@ -488,7 +489,8 @@ extension DatadogContextProvider { device: DeviceInfo, dateProvider: DateProvider, serverDateProvider: ServerDateProvider, - notificationCenter: NotificationCenter + notificationCenter: NotificationCenter, + appStateProvider: AppStateProvider ) { let context = DatadogContext( site: site, @@ -537,6 +539,7 @@ extension DatadogContextProvider { DispatchQueue.main.async { // must be call on the main thread to read `UIApplication.State` let applicationStatePublisher = ApplicationStatePublisher( + appStateProvider: appStateProvider, notificationCenter: notificationCenter, dateProvider: dateProvider ) diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 4959738101..9d13a4f3e7 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -229,6 +229,9 @@ public enum Datadog { /// The default notification center used for subscribing to app lifecycle events and system notifications. internal var notificationCenter: NotificationCenter = .default + + /// The default application state provider for accessing [application state](https://developer.apple.com/documentation/uikit/uiapplication/state). + internal var appStateProvider: AppStateProvider = DefaultAppStateProvider() } /// Verbosity level of Datadog SDK. Can be used for debugging purposes. diff --git a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift index 442430d0e0..ffa82c98cf 100644 --- a/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift +++ b/DatadogCore/Tests/Datadog/DatadogCore/Context/ApplicationStatePublisherTests.swift @@ -26,7 +26,7 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( - initialState: .mockRandom(), + appStateProvider: AppStateProviderMock(state: .mockRandom()), notificationCenter: notificationCenter, dateProvider: SystemDateProvider() ) @@ -57,7 +57,7 @@ class ApplicationStatePublisherTests: XCTestCase { // Given let publisher = ApplicationStatePublisher( - initialState: .mockRandom(), + appStateProvider: AppStateProviderMock(state: .mockRandom()), notificationCenter: notificationCenter, dateProvider: RelativeDateProvider(startingFrom: .mockRandomInThePast(), advancingBySeconds: 1.0) ) diff --git a/DatadogInternal/Sources/Context/AppStateProvider.swift b/DatadogInternal/Sources/Context/AppStateProvider.swift new file mode 100644 index 0000000000..6d580a5008 --- /dev/null +++ b/DatadogInternal/Sources/Context/AppStateProvider.swift @@ -0,0 +1,50 @@ +/* + * 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. + */ + +/// A protocol that provides access to the current application state. +/// See: https://developer.apple.com/documentation/uikit/uiapplication/state +public protocol AppStateProvider: Sendable { + /// The current application state. + /// + /// **Note**: Must be called on the main thread. + var current: AppState { get } +} + +#if canImport(UIKit) + +import UIKit + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state + return AppState(uiKitState) + } +} + +#endif + +#if canImport(WatchKit) + +import WatchKit + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let wkState = WKExtension.dd.shared.applicationState + return AppState(wkState) + } +} + +#endif diff --git a/TestUtilities/Mocks/AppStateProvider.swift b/TestUtilities/Mocks/AppStateProvider.swift new file mode 100644 index 0000000000..b550d175db --- /dev/null +++ b/TestUtilities/Mocks/AppStateProvider.swift @@ -0,0 +1,27 @@ +/* + * 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 + +/// Simple `AppStateProvider` mock that returns given state. +public final class AppStateProviderMock: AppStateProvider { + private let state: ReadWriteLock + + public init(state: AppState = .mockAny()) { + self.state = .init(wrappedValue: state) + } + + public var current: AppState { + get { + // The actual `AppStateProvider` reads `UIApplication.state` and must be accessed on the main thread. + // See: https://developer.apple.com/documentation/uikit/uiapplication/state + precondition(Thread.isMainThread, "The `AppStateProvider` must be accessed on the main thread") + return state.wrappedValue + } + set { state.wrappedValue = newValue } + } +} From 4d11d27df13b5a6a6d7a4c8ec7f6a89576a58b89 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 23 Oct 2024 11:02:03 +0200 Subject: [PATCH 07/38] RUM-6698 chore: Refine session matcher output formatting --- .../Tests/Matchers/RUMSessionMatcher.swift | 267 ++++++++++++++---- 1 file changed, 219 insertions(+), 48 deletions(-) diff --git a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift index b58ecc9804..4c9fccc297 100644 --- a/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift +++ b/DatadogCore/Tests/Matchers/RUMSessionMatcher.swift @@ -46,6 +46,11 @@ internal class RUMSessionMatcher { sessionEventMatchers: eventMatchers ) } + .sorted { session1, session2 in + let startTime1 = session1.views.first?.viewEvents.first?.date ?? 0 + let startTime2 = session2.views.first?.viewEvents.first?.date ?? 0 + return startTime1 < startTime2 + } } // MARK: - View Visits @@ -106,6 +111,21 @@ internal class RUMSessionMatcher { let errorEventMatchers: [RUMEventMatcher] let longTaskEventMatchers: [RUMEventMatcher] + /// `RUMView` events tracked in this session. + let viewEvents: [RUMViewEvent] + + /// `RUMAction` events tracked in this session. + let actionEvents: [RUMActionEvent] + + /// `RUMResource` events tracked in this session. + let resourceEvents: [RUMResourceEvent] + + /// `RUMError` events tracked in this session. + let errorEvents: [RUMErrorEvent] + + /// `RUMLongTask` events tracked in this session. + let longTaskEvents: [RUMLongTaskEvent] + private init(applicationID: String, sessionID: String, sessionEventMatchers: [RUMEventMatcher]) throws { // Sort events so they follow increasing time order let sessionEventOrderedByTime = try sessionEventMatchers.sorted { firstEvent, secondEvent in @@ -257,6 +277,11 @@ internal class RUMSessionMatcher { } self.views = visitsEventOrderedByTime + self.viewEvents = viewEvents + self.actionEvents = actionEvents + self.resourceEvents = resourceEvents + self.errorEvents = errorEvents + self.longTaskEvents = longTaskEvents } /// Checks if this session contains a view with a specific ID. @@ -424,6 +449,15 @@ extension Array where Element == RUMSessionMatcher { } return self[0] } + + /// Returns the only two sessions in this array. + /// Throws if there are more or less than 2 sessions in this array. + func takeTwo() throws -> (RUMSessionMatcher, RUMSessionMatcher) { + guard count == 2 else { + throw RUMSessionConsistencyException(description: "Expected 2 sessions, but found \(count)") + } + return (self[0], self[1]) + } } extension Array where Element == RUMSessionMatcher.View { @@ -498,79 +532,216 @@ extension RUMSessionMatcher { // MARK: - Debugging +extension RUMSessionMatcher.View { + /// The start of this view (as timestamp; milliseconds) defined as the start timestamp of the earliest view event in this view. + var startTimestampMs: Int64 { viewEvents.map({ $0.date }).min() ?? 0 } +} + extension RUMSessionMatcher: CustomStringConvertible { - var description: String { - var description = "[🎞 RUM session (application.id: \(applicationID), session.id: \(sessionID), number of views: \(views.count))]" + var description: String { renderSession() } + + /// The start of this session (as timestamp; milliseconds) defined as the start timestamp of the earliest view in this session. + private var sessionStartTimestampMs: Int64 { viewEvents.map({ $0.date }).min() ?? 0 } + + /// The start of this session (as timestamp; nanoseconds) defined as the start timestamp of the earliest view in this session. + private var sessionStartTimestampNs: Int64 { sessionStartTimestampMs * 1_000_000 } + + /// The end of this session (as timestamp; nanoseconds) defined as the end timestamp of the latest view in this session. + private var sessionEndTimestampNs: Int64 { viewEvents.map({ $0.date * 1_000_000 + $0.view.timeSpent }).max() ?? 0 } + + private func renderSession() -> String { + var output = renderBox(string: "🎞 RUM session") + output += renderAttributesBox( + attributes: [ + ("application.id", applicationID), + ("id", sessionID), + ("views.count", "\(views.count)"), + ("start", prettyDate(timestampMs: sessionStartTimestampMs)), + ("duration", pretty(nanoseconds: sessionEndTimestampNs - sessionStartTimestampNs)), + ] + ) views.forEach { view in - description += "\n\(describe(viewVisit: view))" + output += render(view: view) } - return description + output += renderClosingLine() + return output } - private func describe(viewVisit: View) -> String { - guard let lastViewEvent = viewVisit.viewEvents.last else { - return " → [⛔️ Invalid View - it has no view events]" + private func render(view: View) -> String { + guard let lastViewEvent = view.viewEvents.last else { + return renderBox(string: "⛔️ Invalid View - it has no view events") } - var description = " → [📸 View (name: '\(viewVisit.name ?? "nil")', id: \(viewVisit.viewID), duration: \(seconds(from: lastViewEvent.view.timeSpent)) actions.count: \(lastViewEvent.view.action.count), resources.count: \(lastViewEvent.view.resource.count), errors.count: \(lastViewEvent.view.error.count), longTask.count: \(lastViewEvent.view.longTask?.count ?? 0), frozenFrames.count: \(lastViewEvent.view.frozenFrame?.count ?? 0)]" + var output = renderBox(string: "📸 RUM View (\(view.name ?? "nil"))") + output += renderAttributesBox( + attributes: [ + ("name", view.name ?? "nil"), + ("id", view.viewID), + ("date", prettyDate(timestampMs: lastViewEvent.date)), + ("date (relative in session)", pretty(milliseconds: lastViewEvent.date - sessionStartTimestampMs)), + ("duration", pretty(nanoseconds: lastViewEvent.view.timeSpent)), + ("event counts", "view (\(view.viewEvents.count)), action (\(view.actionEvents.count)), resource (\(view.resourceEvents.count)), error (\(view.errorEvents.count)), long task (\(view.longTaskEvents.count))"), + ] + ) - if !viewVisit.actionEvents.isEmpty { - description += "\n → action events:" - description += "\n\(describe(actionEvents: viewVisit.actionEvents))" + for action in view.actionEvents { + output += renderEmptyLine() + output += render(event: action, in: view) } - if !viewVisit.resourceEvents.isEmpty { - description += "\n → resource events:" - description += "\n\(describe(resourceEvents: viewVisit.resourceEvents))" + for resource in view.resourceEvents { + output += renderEmptyLine() + output += render(event: resource, in: view) } - if !viewVisit.errorEvents.isEmpty { - description += "\n → error events:" - description += "\n\(describe(errorEvents: viewVisit.errorEvents))" + for error in view.errorEvents { + output += renderEmptyLine() + output += render(event: error, in: view) } - if !viewVisit.longTaskEvents.isEmpty { - description += "\n → long task events:" - description += "\n\(describe(longTaskEvents: viewVisit.longTaskEvents))" + for longTask in view.longTaskEvents { + output += renderEmptyLine() + output += render(event: longTask, in: view) } - return description + output += renderEmptyLine() + return output + } + + private func render(event: RUMActionEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("▶️ RUM Action", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("name", event.action.target?.name ?? "nil"), + ("type", "\(event.action.type)"), + ("loading.time", "\(event.action.loadingTime.flatMap({ pretty(nanoseconds: $0) }) ?? "nil")"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMResourceEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🌎 RUM Resource", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("url", event.resource.url), + ("method", "\(event.resource.method.flatMap({ "\($0.rawValue)" }) ?? "nil")"), + ("status.code", "\(event.resource.statusCode.flatMap({ "\($0)" }) ?? "nil")"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMErrorEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🧯 RUM Error", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("message", event.error.message), + ("type", event.error.type ?? "nil"), + ], + prefix: "→", + indentationLevel: 3 + ) + return output + } + + private func render(event: RUMLongTaskEvent, in view: View) -> String { + var output = renderAttributesBox(attributes: [("🐌 RUM Long Task", "")], indentationLevel: 2) + output += renderAttributesBox( + attributes: [ + ("date (relative in view)", pretty(milliseconds: event.date - view.startTimestampMs)), + ("duration", pretty(nanoseconds: event.longTask.duration)), + ], + prefix: "→", + indentationLevel: 3 + ) + return output } - private func describe(actionEvents: [RUMActionEvent]) -> String { - return actionEvents - .map { event in - " → [▶️ Action (name: \(event.action.target?.name ?? "(null)"), type: \(event.action.type)]" - } - .joined(separator: "\n") + // MARK: - Rendering helpers + + private static let rendererWidth = 90 + + private func renderBox(string: String) -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder1 = "+" + String(repeating: "-", count: width - 2) + "+" + let horizontalBorder2 = "|" + String(repeating: "-", count: width - 2) + "|" + let visualWidth = (string as NSString).length + let padding = (width - 2 - visualWidth) / 2 + let leftPadding = String(repeating: " ", count: max(0, padding)) + let rightPadding = String(repeating: " ", count: max(0, width - 2 - visualWidth - padding)) + + let contentLine = "|\(leftPadding)\(string)\(rightPadding)|" + + return """ + \(horizontalBorder1) + \(contentLine) + \(horizontalBorder2)\n + """ } - private func describe(resourceEvents: [RUMResourceEvent]) -> String { - return resourceEvents - .map { event in - " → [🌎 Resource (url: \(event.resource.url), method: \(event.resource.method.flatMap({ "\($0.rawValue)" }) ?? "(null)"), statusCode: \(event.resource.statusCode.flatMap({ "\($0)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderAttributesBox(attributes: [(String, String)], prefix: String = "", indentationLevel: Int = 0) -> String { + let width = RUMSessionMatcher.rendererWidth + let indentation = String(repeating: " ", count: indentationLevel) + + let contentLines = attributes.map { key, value in + let lineContent = "\(indentation)\(prefix) \(key): \(value)" + let visualWidth = (lineContent as NSString).length + let padding = max(0, width - 2 - visualWidth) + let rightPadding = String(repeating: " ", count: padding) + return "|\(lineContent)\(rightPadding)|" + } + + return """ + \(contentLines.joined(separator: "\n"))\n + """ } - private func describe(errorEvents: [RUMErrorEvent]) -> String { - return errorEvents - .map { event in - " → [🧯 Error (message: \(event.error.message), type: \(event.error.type ?? "(null)"), resource: \(event.error.resource.flatMap({ "\($0.url)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderEmptyLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "|" + String(repeating: " ", count: width - 2) + "|" + return horizontalBorder + "\n" } - private func describe(longTaskEvents: [RUMLongTaskEvent]) -> String { - return longTaskEvents - .map { event in - " → [🐌 LongTask (duration: \(seconds(from: event.longTask.duration)), isFrozenFrame: \(event.longTask.isFrozenFrame.flatMap({ "\($0)" }) ?? "(null)")]" - } - .joined(separator: "\n") + private func renderClosingLine() -> String { + let width = RUMSessionMatcher.rendererWidth + let horizontalBorder = "+" + String(repeating: "-", count: width - 2) + "+" + return horizontalBorder + "\n" + } + + private func pretty(milliseconds: Int64) -> String { + pretty(nanoseconds: milliseconds * 1_000_000) } - private func seconds(from nanoseconds: Int64) -> String { - let prettySeconds = (round((Double(nanoseconds) / 1_000_000_000) * 100)) / 100 - return "\(prettySeconds)s" + private func pretty(nanoseconds: Int64) -> String { + if nanoseconds >= 1_000_000_000 { + let seconds = round((Double(nanoseconds) / 1_000_000_000) * 100) / 100 + return "\(seconds)s" + } else if nanoseconds >= 1_000_000 { + let milliseconds = round((Double(nanoseconds) / 1_000_000) * 100) / 100 + return "\(milliseconds)ms" + } else { + return "\(nanoseconds)ns" + } + } + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + return formatter + }() + + private func prettyDate(timestampMs: Int64) -> String { + let timestampSec = TimeInterval(timestampMs) / 1_000 + let date = Date(timeIntervalSince1970: timestampSec) + return RUMSessionMatcher.dateFormatter.string(from: date) } } From 81e66466b4185d4ba02d95ccc6090df5faf0cf8b Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 30 Oct 2024 09:45:58 +0100 Subject: [PATCH 08/38] RUM-6698 CR feedback --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 -- .../Core/Context/BatteryStatusPublisher.swift | 4 +- .../Core/Context/LowPowerModePublisher.swift | 2 +- DatadogCore/Sources/Core/DatadogCore.swift | 92 +----------------- DatadogCore/Sources/Datadog.swift | 97 +++++++++++++++++++ .../Sources/Context/AppState.swift | 43 +++++++- .../Sources/Context/AppStateProvider.swift | 50 ---------- .../Sources/Context/DeviceInfo.swift | 2 +- 8 files changed, 144 insertions(+), 152 deletions(-) delete mode 100644 DatadogInternal/Sources/Context/AppStateProvider.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 178d377a0b..4125d43a72 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -307,8 +307,6 @@ 61133C702423993200786299 /* DatadogCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* DatadogCore.framework */; }; 6115299725E3BEF9004F740E /* UIKitExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6115299625E3BEF9004F740E /* UIKitExtensionsTests.swift */; }; 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */; }; - 6117A4E12CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */; }; - 6117A4E22CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */; }; 6117A4E42CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; 6117A4E52CCBB54500EBBB6F /* AppStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */; }; 61181CDC2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */; }; @@ -2378,7 +2376,6 @@ 611529A425E3DD51004F740E /* ValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisher.swift; sourceTree = ""; }; 611529AD25E3E429004F740E /* ValuePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePublisherTests.swift; sourceTree = ""; }; 611720D42524D9FB00634D9E /* DDURLSessionDelegate+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDURLSessionDelegate+objc.swift"; sourceTree = ""; }; - 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateProvider.swift; sourceTree = ""; }; 6117A4E32CCBB54500EBBB6F /* AppStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateProvider.swift; sourceTree = ""; }; 61181CDB2BF35BC000632A7A /* FatalErrorContextNotifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorContextNotifierTests.swift; sourceTree = ""; }; 61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMActionsHandler.swift; sourceTree = ""; }; @@ -5925,7 +5922,6 @@ children = ( E2AA55E62C32C6D9002FEF28 /* ApplicationNotifications.swift */, D23039B3298D5235001A1FA3 /* AppState.swift */, - 6117A4E02CCB95DF00EBBB6F /* AppStateProvider.swift */, D23039B4298D5235001A1FA3 /* UserInfo.swift */, D23039B5298D5235001A1FA3 /* BatteryStatus.swift */, D23039B6298D5235001A1FA3 /* CarrierInfo.swift */, @@ -8752,7 +8748,6 @@ D23039E6298D5236001A1FA3 /* Sysctl.swift in Sources */, 614A708E2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, D2160CF429C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, - 6117A4E12CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */, D23039E1298D5236001A1FA3 /* AppState.swift in Sources */, D2DE63532A30A7CA00441A54 /* CoreRegistry.swift in Sources */, E2AA55EA2C32C76A002FEF28 /* WatchKitExtensions.swift in Sources */, @@ -9743,7 +9738,6 @@ D2DA236B298D57AA00C6C7E6 /* Sysctl.swift in Sources */, 614A708F2BF754D800D9AF42 /* ImmutableRequest.swift in Sources */, D2160CF529C0EDFC00FAA9A5 /* UploadPerformancePreset.swift in Sources */, - 6117A4E22CCB95DF00EBBB6F /* AppStateProvider.swift in Sources */, D2DA236C298D57AA00C6C7E6 /* AppState.swift in Sources */, D2DE63542A30A7CA00441A54 /* CoreRegistry.swift in Sources */, E2AA55EC2C32C78B002FEF28 /* WatchKitExtensions.swift in Sources */, diff --git a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift index 041c8a49e6..1b1c529ff3 100644 --- a/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift +++ b/DatadogCore/Sources/Core/Context/BatteryStatusPublisher.swift @@ -25,10 +25,10 @@ internal final class BatteryStatusPublisher: ContextValuePublisher { /// /// - Parameters: /// - notificationCenter: The notification center for observing the `UIDevice` battery changes, - /// - device: The `UIDevice` instance. `.current` by default. + /// - device: The `UIDevice` instance. init( notificationCenter: NotificationCenter, - device: UIDevice = .current + device: UIDevice ) { self.device = device self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift index b62027f207..7e314439fe 100644 --- a/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift +++ b/DatadogCore/Sources/Core/Context/LowPowerModePublisher.swift @@ -23,7 +23,7 @@ internal final class LowPowerModePublisher: ContextValuePublisher { /// - processInfo: The process for reading the initial `isLowPowerModeEnabled`. init( notificationCenter: NotificationCenter, - processInfo: ProcessInfo = .processInfo + processInfo: ProcessInfo ) { self.initialValue = processInfo.isLowPowerModeEnabled self.notificationCenter = notificationCenter diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index 0e31f33914..2bc02b2753 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -71,93 +71,6 @@ internal final class DatadogCore { /// Maximum number of batches per upload. internal let maxBatchesPerUpload: Int - convenience init( - configuration: Datadog.Configuration, - trackingConsent: TrackingConsent, - instanceName: String - ) throws { - let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) - if debug { - consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) - Datadog.verbosityLevel = .debug - } - - let applicationVersion = configuration.additionalConfiguration[CrossPlatformAttributes.version] as? String - ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String - ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String - ?? "0.0.0" - - let applicationBuildNumber = configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String - ?? "0" - - let bundleName = configuration.bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as? String - let bundleType = BundleType(bundle: configuration.bundle) - let bundleIdentifier = configuration.bundle.bundleIdentifier ?? "unknown" - let service = configuration.service ?? configuration.bundle.bundleIdentifier ?? "ios" - let source = configuration.additionalConfiguration[CrossPlatformAttributes.ddsource] as? String ?? "ios" - let variant = configuration.additionalConfiguration[CrossPlatformAttributes.variant] as? String - let sdkVersion = configuration.additionalConfiguration[CrossPlatformAttributes.sdkVersion] as? String ?? __sdkVersion - let buildId = configuration.additionalConfiguration[CrossPlatformAttributes.buildId] as? String - let nativeSourceType = configuration.additionalConfiguration[CrossPlatformAttributes.nativeSourceType] as? String - - let performance = PerformancePreset( - batchSize: debug ? .small : configuration.batchSize, - uploadFrequency: debug ? .frequent : configuration.uploadFrequency, - bundleType: bundleType - ) - let isRunFromExtension = bundleType == .iOSAppExtension - - self.init( - directory: try CoreDirectory( - in: configuration.systemDirectory(), - instanceName: instanceName, - site: configuration.site - ), - dateProvider: configuration.dateProvider, - initialConsent: trackingConsent, - performance: performance, - httpClient: configuration.httpClientFactory(configuration.proxyConfiguration), - encryption: configuration.encryption, - contextProvider: DatadogContextProvider( - site: configuration.site, - clientToken: configuration.clientToken, - service: service, - env: configuration.env, - version: applicationVersion, - buildNumber: applicationBuildNumber, - buildId: buildId, - variant: variant, - source: source, - nativeSourceOverride: nativeSourceType, - sdkVersion: sdkVersion, - ciAppOrigin: CITestIntegration.active?.origin, - applicationName: bundleName ?? bundleType.rawValue, - applicationBundleIdentifier: bundleIdentifier, - applicationBundleType: bundleType, - applicationVersion: applicationVersion, - sdkInitDate: configuration.dateProvider.now, - device: DeviceInfo(), - dateProvider: configuration.dateProvider, - serverDateProvider: configuration.serverDateProvider, - notificationCenter: configuration.notificationCenter, - appStateProvider: configuration.appStateProvider - ), - applicationVersion: applicationVersion, - maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, - backgroundTasksEnabled: configuration.backgroundTasksEnabled, - isRunFromExtension: isRunFromExtension - ) - - telemetry.configuration( - backgroundTasksEnabled: configuration.backgroundTasksEnabled, - batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), - batchSize: performance.uploaderWindow.toInt64Milliseconds, - batchUploadFrequency: performance.minUploadDelay.toInt64Milliseconds, - useLocalEncryption: configuration.encryption != nil, - useProxy: configuration.proxyConfiguration != nil - ) - } - /// Creates a core instance. /// /// - Parameters: @@ -487,6 +400,7 @@ extension DatadogContextProvider { applicationVersion: String, sdkInitDate: Date, device: DeviceInfo, + processInfo: ProcessInfo, dateProvider: DateProvider, serverDateProvider: ServerDateProvider, notificationCenter: NotificationCenter, @@ -531,8 +445,8 @@ extension DatadogContextProvider { #endif #if os(iOS) && !targetEnvironment(simulator) - subscribe(\.batteryStatus, to: BatteryStatusPublisher(notificationCenter: notificationCenter)) - subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher(notificationCenter: notificationCenter)) + subscribe(\.batteryStatus, to: BatteryStatusPublisher(notificationCenter: notificationCenter, device: .current)) + subscribe(\.isLowPowerModeEnabled, to: LowPowerModePublisher(notificationCenter: notificationCenter, processInfo: processInfo)) #endif #if os(iOS) || os(tvOS) diff --git a/DatadogCore/Sources/Datadog.swift b/DatadogCore/Sources/Datadog.swift index 9d13a4f3e7..a731ff8a09 100644 --- a/DatadogCore/Sources/Datadog.swift +++ b/DatadogCore/Sources/Datadog.swift @@ -478,3 +478,100 @@ private func isValid(clientToken: String) throws { throw ProgrammerError(description: "`clientToken` cannot be empty.") } } + +extension DatadogCore { + /// The primary entry point for creating a `DatadogCore` instance. + /// + /// - Parameters: + /// - configuration: A configuration object that encapsulates both user-defined options and internal dependencies + /// passed to SDK's downstream components. + /// - trackingConsent: The user's consent regarding data tracking for the SDK. + /// - instanceName: A unique name for this SDK instance. + convenience init( + configuration: Datadog.Configuration, + trackingConsent: TrackingConsent, + instanceName: String + ) throws { + let debug = configuration.processInfo.arguments.contains(LaunchArguments.Debug) + if debug { + consolePrint("⚠️ Overriding verbosity, and upload frequency due to \(LaunchArguments.Debug) launch argument", .warn) + Datadog.verbosityLevel = .debug + } + + let applicationVersion = configuration.additionalConfiguration[CrossPlatformAttributes.version] as? String + ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + ?? configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + ?? "0.0.0" + + let applicationBuildNumber = configuration.bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String + ?? "0" + + let bundleName = configuration.bundle.object(forInfoDictionaryKey: "CFBundleExecutable") as? String + let bundleType = BundleType(bundle: configuration.bundle) + let bundleIdentifier = configuration.bundle.bundleIdentifier ?? "unknown" + let service = configuration.service ?? configuration.bundle.bundleIdentifier ?? "ios" + let source = configuration.additionalConfiguration[CrossPlatformAttributes.ddsource] as? String ?? "ios" + let variant = configuration.additionalConfiguration[CrossPlatformAttributes.variant] as? String + let sdkVersion = configuration.additionalConfiguration[CrossPlatformAttributes.sdkVersion] as? String ?? __sdkVersion + let buildId = configuration.additionalConfiguration[CrossPlatformAttributes.buildId] as? String + let nativeSourceType = configuration.additionalConfiguration[CrossPlatformAttributes.nativeSourceType] as? String + + let performance = PerformancePreset( + batchSize: debug ? .small : configuration.batchSize, + uploadFrequency: debug ? .frequent : configuration.uploadFrequency, + bundleType: bundleType + ) + let isRunFromExtension = bundleType == .iOSAppExtension + + self.init( + directory: try CoreDirectory( + in: configuration.systemDirectory(), + instanceName: instanceName, + site: configuration.site + ), + dateProvider: configuration.dateProvider, + initialConsent: trackingConsent, + performance: performance, + httpClient: configuration.httpClientFactory(configuration.proxyConfiguration), + encryption: configuration.encryption, + contextProvider: DatadogContextProvider( + site: configuration.site, + clientToken: configuration.clientToken, + service: service, + env: configuration.env, + version: applicationVersion, + buildNumber: applicationBuildNumber, + buildId: buildId, + variant: variant, + source: source, + nativeSourceOverride: nativeSourceType, + sdkVersion: sdkVersion, + ciAppOrigin: CITestIntegration.active?.origin, + applicationName: bundleName ?? bundleType.rawValue, + applicationBundleIdentifier: bundleIdentifier, + applicationBundleType: bundleType, + applicationVersion: applicationVersion, + sdkInitDate: configuration.dateProvider.now, + device: DeviceInfo(processInfo: configuration.processInfo), + processInfo: configuration.processInfo, + dateProvider: configuration.dateProvider, + serverDateProvider: configuration.serverDateProvider, + notificationCenter: configuration.notificationCenter, + appStateProvider: configuration.appStateProvider + ), + applicationVersion: applicationVersion, + maxBatchesPerUpload: configuration.batchProcessingLevel.maxBatchesPerUpload, + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + isRunFromExtension: isRunFromExtension + ) + + telemetry.configuration( + backgroundTasksEnabled: configuration.backgroundTasksEnabled, + batchProcessingLevel: Int64(exactly: configuration.batchProcessingLevel.maxBatchesPerUpload), + batchSize: performance.uploaderWindow.toInt64Milliseconds, + batchUploadFrequency: performance.minUploadDelay.toInt64Milliseconds, + useLocalEncryption: configuration.encryption != nil, + useProxy: configuration.proxyConfiguration != nil + ) + } +} diff --git a/DatadogInternal/Sources/Context/AppState.swift b/DatadogInternal/Sources/Context/AppState.swift index 79923d6118..080a24c428 100644 --- a/DatadogInternal/Sources/Context/AppState.swift +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -6,6 +6,15 @@ import Foundation +/// A protocol that provides access to the current application state. +/// See: https://developer.apple.com/documentation/uikit/uiapplication/state +public protocol AppStateProvider: Sendable { + /// The current application state. + /// + /// **Note**: Must be called on the main thread. + var current: AppState { get } +} + /// Application state. public enum AppState: Codable, PassthroughAnyCodable { /// The app is running in the foreground and currently receiving events. @@ -146,14 +155,42 @@ extension AppStateHistory { import UIKit -#if canImport(WatchKit) +public typealias ApplicationState = UIApplication.State + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state + return AppState(uiKitState) + } +} + +#elseif canImport(WatchKit) + import WatchKit public typealias ApplicationState = WKApplicationState -#else -public typealias ApplicationState = UIApplication.State + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + + /// Gets the current application state. + /// + /// **Note**: Must be called on the main thread. + public var current: AppState { + let wkState = WKExtension.dd.shared.applicationState + return AppState(wkState) + } +} + #endif +#if canImport(UIKit) || canImport(WatchKit) + extension AppState { public init(_ state: ApplicationState) { switch state { diff --git a/DatadogInternal/Sources/Context/AppStateProvider.swift b/DatadogInternal/Sources/Context/AppStateProvider.swift deleted file mode 100644 index 6d580a5008..0000000000 --- a/DatadogInternal/Sources/Context/AppStateProvider.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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. - */ - -/// A protocol that provides access to the current application state. -/// See: https://developer.apple.com/documentation/uikit/uiapplication/state -public protocol AppStateProvider: Sendable { - /// The current application state. - /// - /// **Note**: Must be called on the main thread. - var current: AppState { get } -} - -#if canImport(UIKit) - -import UIKit - -public struct DefaultAppStateProvider: AppStateProvider { - public init() {} - - /// Gets the current application state. - /// - /// **Note**: Must be called on the main thread. - public var current: AppState { - let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state - return AppState(uiKitState) - } -} - -#endif - -#if canImport(WatchKit) - -import WatchKit - -public struct DefaultAppStateProvider: AppStateProvider { - public init() {} - - /// Gets the current application state. - /// - /// **Note**: Must be called on the main thread. - public var current: AppState { - let wkState = WKExtension.dd.shared.applicationState - return AppState(wkState) - } -} - -#endif diff --git a/DatadogInternal/Sources/Context/DeviceInfo.swift b/DatadogInternal/Sources/Context/DeviceInfo.swift index 9c63c2463f..bfa73edfe5 100644 --- a/DatadogInternal/Sources/Context/DeviceInfo.swift +++ b/DatadogInternal/Sources/Context/DeviceInfo.swift @@ -81,7 +81,7 @@ extension DeviceInfo { /// - processInfo: The current process information. /// - device: The device description. public init( - processInfo: ProcessInfo = .processInfo, + processInfo: ProcessInfo, device: _UIDevice = .dd.current, sysctl: SysctlProviding = Sysctl() ) { From 72a26549639fd0085ef3bac6c8796c9379358c1a Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 30 Oct 2024 10:01:30 +0100 Subject: [PATCH 09/38] RUM-6698 Fix macOS build --- .../Sources/Context/AppState.swift | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/DatadogInternal/Sources/Context/AppState.swift b/DatadogInternal/Sources/Context/AppState.swift index 080a24c428..271d257eab 100644 --- a/DatadogInternal/Sources/Context/AppState.swift +++ b/DatadogInternal/Sources/Context/AppState.swift @@ -151,11 +151,9 @@ extension AppStateHistory { } } -#if canImport(UIKit) +#if canImport(WatchKit) -import UIKit - -public typealias ApplicationState = UIApplication.State +import WatchKit public struct DefaultAppStateProvider: AppStateProvider { public init() {} @@ -164,16 +162,26 @@ public struct DefaultAppStateProvider: AppStateProvider { /// /// **Note**: Must be called on the main thread. public var current: AppState { - let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state - return AppState(uiKitState) + let wkState = WKExtension.dd.shared.applicationState + return AppState(wkState) } } -#elseif canImport(WatchKit) +extension AppState { + public init(_ state: WKApplicationState) { + switch state { + case .active: self = .active + case .inactive: self = .inactive + case .background: self = .background + @unknown default: + self = .active // in case a new state is introduced, default to most expected state + } + } +} -import WatchKit +#elseif canImport(UIKit) -public typealias ApplicationState = WKApplicationState +import UIKit public struct DefaultAppStateProvider: AppStateProvider { public init() {} @@ -182,28 +190,27 @@ public struct DefaultAppStateProvider: AppStateProvider { /// /// **Note**: Must be called on the main thread. public var current: AppState { - let wkState = WKExtension.dd.shared.applicationState - return AppState(wkState) + let uiKitState = UIApplication.dd.managedShared?.applicationState ?? .active // fallback to most expected state + return AppState(uiKitState) } } -#endif - -#if canImport(UIKit) || canImport(WatchKit) - extension AppState { - public init(_ state: ApplicationState) { + public init(_ state: UIApplication.State) { switch state { - case .active: - self = .active - case .inactive: - self = .inactive - case .background: - self = .background - @unknown default: - self = .active // in case a new state is introduced, we rather want to fallback to most expected state + case .active: self = .active + case .inactive: self = .inactive + case .background: self = .background + @unknown default: self = .active // in case a new state is introduced, default to most expected state } } } +#else // macOS (no UIKit and no WatchKit) + +public struct DefaultAppStateProvider: AppStateProvider { + public init() {} + public let current: AppState = .active +} + #endif From 356993e5e561a88f02ed6e124eb1d142a40f3d4b Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Tue, 29 Oct 2024 11:29:34 +0100 Subject: [PATCH 10/38] RUM-6862 Add touch overrides cache clean-up mechanism --- .../TouchIdentifierGenerator.swift | 8 ++++++- .../WindowTouchSnapshotProducer.swift | 18 +++++----------- .../WindowTouchSnapshotProducerTests.swift | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift index 197933c081..14895f9ef6 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift @@ -74,11 +74,17 @@ internal final class TouchIdentifierGenerator { // MARK: - UIView tagging fileprivate var associatedTouchIdentifierKey: UInt8 = 1 +fileprivate var associatedTouchPrivacyOverrideKey: UInt8 = 2 -private extension UITouch { +internal extension UITouch { var identifier: TouchIdentifier? { set { objc_setAssociatedObject(self, &associatedTouchIdentifierKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } get { objc_getAssociatedObject(self, &associatedTouchIdentifierKey) as? TouchIdentifier } } + + var touchPrivacyOverride: TouchPrivacyLevel? { + set { objc_setAssociatedObject(self, &associatedTouchPrivacyOverrideKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + get { objc_getAssociatedObject(self, &associatedTouchPrivacyOverrideKey) as? TouchPrivacyLevel } + } } #endif diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift index 3c1a763fae..e377965d4d 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift @@ -14,8 +14,6 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle private let windowObserver: AppWindowObserver /// Generates persisted IDs for `UITouch` objects. private let idsGenerator = TouchIdentifierGenerator() - /// Keeps track of the privacy override for each touch event - private var overrideForTouch: [TouchIdentifier: TouchPrivacyLevel] = [:] /// Touches recorded since last call to `takeSnapshot()` private var buffer: [TouchSnapshot.Touch] = [] @@ -34,13 +32,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle } // Filter the buffer to only include touches that should be recorded - let shouldRecord = shouldRecordTouch(touch.id, in: context) - - // Clean up cache when the touch ends - if touch.phase == .up { - overrideForTouch.removeValue(forKey: touch.id) - } - + let shouldRecord = shouldRecordTouch(updatedTouch, in: context) return shouldRecord ? updatedTouch : nil } @@ -77,7 +69,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle // Capture the touch privacy override when the touch begins if phase == .down, let privacyOverride = resolveTouchOverride(for: touch) { - overrideForTouch[touchId] = privacyOverride + touch.touchPrivacyOverride = privacyOverride } buffer.append( @@ -86,7 +78,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle phase: phase, date: Date(), position: touch.location(in: window), - touchOverride: overrideForTouch[touchId] + touchOverride: touch.touchPrivacyOverride ) ) } @@ -97,9 +89,9 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle /// Otherwise, the global touch privacy setting is applied. /// - Parameter touchId: The unique identifier for the touch event. /// - Returns: `true` if the touch should be recorded, `false` otherwise. - internal func shouldRecordTouch(_ touchId: TouchIdentifier, in context: Recorder.Context + internal func shouldRecordTouch(_ touch: TouchSnapshot.Touch, in context: Recorder.Context ) -> Bool { - let privacy: TouchPrivacyLevel = overrideForTouch[touchId] ?? context.touchPrivacy + let privacy: TouchPrivacyLevel = touch.touchOverride ?? context.touchPrivacy return privacy == .show } diff --git a/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift b/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift index 5c948f0710..091f84c32f 100644 --- a/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducerTests.swift @@ -244,5 +244,26 @@ class WindowTouchSnapshotProducerTests: XCTestCase { XCTAssertNotNil(snapshot, "Touches in a view with touch privacy override `.show` should be recorded even when global setting is `.hide`") XCTAssertEqual(snapshot?.touches.count, 1, "It should record one touch event") } + + // MARK: Touch Override Cache Tests + func testTouchPrivacyOverrideIsNilByDefault() { + // Given + let touch = UITouchMock() + + // Then + XCTAssertNil(touch.touchPrivacyOverride, "The associated touchPrivacyOverride should be nil by default") + } + + func testSettingAndGettingTouchPrivacyOverride() { + // Given + let touch = UITouchMock() + let expectedOverride: TouchPrivacyLevel = .show + + // When + touch.touchPrivacyOverride = expectedOverride + + // Then + XCTAssertEqual(touch.touchPrivacyOverride, expectedOverride, "The associated touchPrivacyOverride should be correctly set and retrieved") + } } #endif From d4e90cecfae081faa17932a09dc522084c008773 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Wed, 30 Oct 2024 18:02:06 +0100 Subject: [PATCH 11/38] RUM-6862 Move properties to DatadogExtended --- .../TouchIdentifierGenerator.swift | 23 +++++++++++-------- .../WindowTouchSnapshotProducer.swift | 5 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift index 14895f9ef6..b255e0e948 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift @@ -6,6 +6,7 @@ #if os(iOS) import UIKit +import DatadogInternal /// Unique identifier of a touch. /// It is used to mark `UITouch` objects in order to track their identity without capturing reference. @@ -41,17 +42,18 @@ internal final class TouchIdentifierGenerator { case .down: return persistNextID(in: touch) case .move: - guard let persistedID = touch.identifier else { + guard let persistedID = touch.dd.identifier else { // It means the touch began before SR was enabled → persit next ID in this touch: return persistNextID(in: touch) } return persistedID case .up: - guard let persistedID = touch.identifier else { + guard let persistedID = touch.dd.identifier else { // It means the touch began before SR was enabled → only return next ID as we know the touch is ending: return getNextID() } - touch.identifier = nil + var ddTouch = touch.dd + ddTouch.identifier = nil return persistedID default: return persistNextID(in: touch) @@ -60,7 +62,8 @@ internal final class TouchIdentifierGenerator { private func persistNextID(in touch: UITouch) -> TouchIdentifier { let newID = getNextID() - touch.identifier = newID + var ddTouch = touch.dd + ddTouch.identifier = newID return newID } @@ -76,15 +79,17 @@ internal final class TouchIdentifierGenerator { fileprivate var associatedTouchIdentifierKey: UInt8 = 1 fileprivate var associatedTouchPrivacyOverrideKey: UInt8 = 2 -internal extension UITouch { +extension UITouch: DatadogExtended { } + +extension DatadogExtension where ExtendedType: UITouch { var identifier: TouchIdentifier? { - set { objc_setAssociatedObject(self, &associatedTouchIdentifierKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - get { objc_getAssociatedObject(self, &associatedTouchIdentifierKey) as? TouchIdentifier } + set { objc_setAssociatedObject(type, &associatedTouchIdentifierKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + get { objc_getAssociatedObject(type, &associatedTouchIdentifierKey) as? TouchIdentifier } } var touchPrivacyOverride: TouchPrivacyLevel? { - set { objc_setAssociatedObject(self, &associatedTouchPrivacyOverrideKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - get { objc_getAssociatedObject(self, &associatedTouchPrivacyOverrideKey) as? TouchPrivacyLevel } + set { objc_setAssociatedObject(type, &associatedTouchPrivacyOverrideKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + get { objc_getAssociatedObject(type, &associatedTouchPrivacyOverrideKey) as? TouchPrivacyLevel } } } #endif diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift index e377965d4d..9606179a81 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift @@ -69,7 +69,8 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle // Capture the touch privacy override when the touch begins if phase == .down, let privacyOverride = resolveTouchOverride(for: touch) { - touch.touchPrivacyOverride = privacyOverride + var ddTouch = touch.dd + ddTouch.touchPrivacyOverride = privacyOverride } buffer.append( @@ -78,7 +79,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle phase: phase, date: Date(), position: touch.location(in: window), - touchOverride: touch.touchPrivacyOverride + touchOverride: touch.dd.touchPrivacyOverride ) ) } From 97fce106758dc96b5bff7b9198f31b2b1eb66f35 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Wed, 30 Oct 2024 18:18:26 +0100 Subject: [PATCH 12/38] Revert "RUM-6862 Move properties to DatadogExtended" This reverts commit c4028199b4e36a854c38abe22008559dc1b2e8e7. --- .../TouchIdentifierGenerator.swift | 23 ++++++++----------- .../WindowTouchSnapshotProducer.swift | 5 ++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift index b255e0e948..14895f9ef6 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/TouchSnapshot/TouchIdentifierGenerator.swift @@ -6,7 +6,6 @@ #if os(iOS) import UIKit -import DatadogInternal /// Unique identifier of a touch. /// It is used to mark `UITouch` objects in order to track their identity without capturing reference. @@ -42,18 +41,17 @@ internal final class TouchIdentifierGenerator { case .down: return persistNextID(in: touch) case .move: - guard let persistedID = touch.dd.identifier else { + guard let persistedID = touch.identifier else { // It means the touch began before SR was enabled → persit next ID in this touch: return persistNextID(in: touch) } return persistedID case .up: - guard let persistedID = touch.dd.identifier else { + guard let persistedID = touch.identifier else { // It means the touch began before SR was enabled → only return next ID as we know the touch is ending: return getNextID() } - var ddTouch = touch.dd - ddTouch.identifier = nil + touch.identifier = nil return persistedID default: return persistNextID(in: touch) @@ -62,8 +60,7 @@ internal final class TouchIdentifierGenerator { private func persistNextID(in touch: UITouch) -> TouchIdentifier { let newID = getNextID() - var ddTouch = touch.dd - ddTouch.identifier = newID + touch.identifier = newID return newID } @@ -79,17 +76,15 @@ internal final class TouchIdentifierGenerator { fileprivate var associatedTouchIdentifierKey: UInt8 = 1 fileprivate var associatedTouchPrivacyOverrideKey: UInt8 = 2 -extension UITouch: DatadogExtended { } - -extension DatadogExtension where ExtendedType: UITouch { +internal extension UITouch { var identifier: TouchIdentifier? { - set { objc_setAssociatedObject(type, &associatedTouchIdentifierKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - get { objc_getAssociatedObject(type, &associatedTouchIdentifierKey) as? TouchIdentifier } + set { objc_setAssociatedObject(self, &associatedTouchIdentifierKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + get { objc_getAssociatedObject(self, &associatedTouchIdentifierKey) as? TouchIdentifier } } var touchPrivacyOverride: TouchPrivacyLevel? { - set { objc_setAssociatedObject(type, &associatedTouchPrivacyOverrideKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } - get { objc_getAssociatedObject(type, &associatedTouchPrivacyOverrideKey) as? TouchPrivacyLevel } + set { objc_setAssociatedObject(self, &associatedTouchPrivacyOverrideKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + get { objc_getAssociatedObject(self, &associatedTouchPrivacyOverrideKey) as? TouchPrivacyLevel } } } #endif diff --git a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift index 9606179a81..e377965d4d 100644 --- a/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift +++ b/DatadogSessionReplay/Sources/Recorder/TouchSnapshotProducer/WindowTouchSnapshotProducer.swift @@ -69,8 +69,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle // Capture the touch privacy override when the touch begins if phase == .down, let privacyOverride = resolveTouchOverride(for: touch) { - var ddTouch = touch.dd - ddTouch.touchPrivacyOverride = privacyOverride + touch.touchPrivacyOverride = privacyOverride } buffer.append( @@ -79,7 +78,7 @@ internal class WindowTouchSnapshotProducer: TouchSnapshotProducer, UIEventHandle phase: phase, date: Date(), position: touch.location(in: window), - touchOverride: touch.dd.touchPrivacyOverride + touchOverride: touch.touchPrivacyOverride ) ) } From 4891247be3c367b4b1d5f353ccf8ee7807dabc07 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 17 Oct 2024 13:57:24 +0200 Subject: [PATCH 13/38] RUM-6118 propagate clipping --- Datadog/Datadog.xcodeproj/project.pbxproj | 24 +-- .../Processor/Builders/RecordsBuilder.swift | 2 +- .../Builders/WireframesBuilder.swift | 78 +++++++-- .../Processor/Diffing/Diff+SRWireframes.swift | 15 +- .../Utilities/CGRect+ContentFrame.swift | 156 ------------------ .../Utilities/CGRect+SessionReplay.swift | 112 +++++++++++++ .../Utilities/CGSize+SessionReplay.swift | 54 ++++++ .../UIActivityIndicatorRecorder.swift | 1 + .../NodeRecorders/UIDatePickerRecorder.swift | 2 +- .../NodeRecorders/UIImageViewRecorder.swift | 34 +--- .../NodeRecorders/UILabelRecorder.swift | 1 + .../UINavigationBarRecorder.swift | 1 + .../NodeRecorders/UIPickerViewRecorder.swift | 2 +- .../UIProgressViewRecorder.swift | 4 +- .../NodeRecorders/UISegmentRecorder.swift | 4 +- .../NodeRecorders/UISliderRecorder.swift | 14 +- .../NodeRecorders/UIStepperRecorder.swift | 7 + .../NodeRecorders/UISwitchRecorder.swift | 11 +- .../NodeRecorders/UITabBarRecorder.swift | 1 + .../NodeRecorders/UITextFieldRecorder.swift | 1 + .../NodeRecorders/UITextViewRecorder.swift | 15 +- .../NodeRecorders/UIViewRecorder.swift | 29 ++-- .../UnsupportedViewRecorder.swift | 1 + .../NodeRecorders/WKWebViewRecorder.swift | 1 + .../ViewAttributes+Copy.swift | 57 ------- .../ViewTreeSnapshot/ViewTreeRecorder.swift | 20 ++- .../ViewTreeRecordingContext.swift | 2 + .../ViewTreeSnapshot/ViewTreeSnapshot.swift | 76 +++++---- .../ViewTreeSnapshotBuilder.swift | 3 +- .../Tests/Mocks/RecorderMocks.swift | 47 ++++-- .../Builders/WireframesBuilderTests.swift | 48 +++++- .../Diffing/Diff+SRWireframesTests.swift | 43 ++++- .../Processor/SnapshotProcessorTests.swift | 1 - ....swift => CGRect+SessionReplayTests.swift} | 66 ++++---- .../UIImageViewWireframesBuilderTests.swift | 2 - .../UINavigationBarRecorderTests.swift | 2 +- .../NodeRecorders/UITabBarRecorderTests.swift | 4 +- .../ViewTreeRecorderTests.swift | 82 +++++++-- .../ViewTreeSnapshotTests.swift | 76 ++++----- 39 files changed, 624 insertions(+), 475 deletions(-) delete mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+ContentFrame.swift create mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+SessionReplay.swift create mode 100644 DatadogSessionReplay/Sources/Recorder/Utilities/CGSize+SessionReplay.swift delete mode 100644 DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewAttributes+Copy.swift rename DatadogSessionReplay/Tests/Recorder/Utilties/{CGRect+ContentFrameTests.swift => CGRect+SessionReplayTests.swift} (68%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 4125d43a72..fa14fd014d 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -158,7 +158,7 @@ 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */; }; 61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */; }; 61054E6C2A6EE10A00AAA894 /* SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E172A6EE10A00AAA894 /* SystemColors.swift */; }; - 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */; }; + 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */; }; 61054E6E2A6EE10A00AAA894 /* RecordingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */; }; 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */; }; 61054E702A6EE10A00AAA894 /* TouchSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */; }; @@ -184,7 +184,6 @@ 61054E842A6EE10A00AAA894 /* UITabBarRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */; }; 61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */; }; 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */; }; - 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */; }; 61054E882A6EE10A00AAA894 /* ViewTreeRecordingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */; }; 61054E892A6EE10A00AAA894 /* NodeIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */; }; 61054E8A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */; }; @@ -226,7 +225,7 @@ 61054FA72A6EE1BA00AAA894 /* NodesFlattenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */; }; 61054FA82A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */; }; 61054FAA2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */; }; - 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */; }; + 61054FAC2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */; }; 61054FAD2A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */; }; 61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */; }; 61054FAF2A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */; }; @@ -1155,6 +1154,7 @@ D270CDDE2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */; }; D270CDE02B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; }; + D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */; }; D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; }; D27CBD9A2BB5DBBB00C766AA /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27CBD992BB5DBBB00C766AA /* Mocks.swift */; }; @@ -2209,7 +2209,7 @@ 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplay.swift"; sourceTree = ""; }; 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CFType+Safety.swift"; sourceTree = ""; }; 61054E172A6EE10A00AAA894 /* SystemColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemColors.swift; sourceTree = ""; }; - 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+ContentFrame.swift"; sourceTree = ""; }; + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+SessionReplay.swift"; sourceTree = ""; }; 61054E192A6EE10A00AAA894 /* RecordingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinator.swift; sourceTree = ""; }; 61054E1B2A6EE10A00AAA894 /* UIApplicationSwizzler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplicationSwizzler.swift; sourceTree = ""; }; 61054E1C2A6EE10A00AAA894 /* TouchSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchSnapshotProducer.swift; sourceTree = ""; }; @@ -2235,7 +2235,6 @@ 61054E342A6EE10A00AAA894 /* UITabBarRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITabBarRecorder.swift; sourceTree = ""; }; 61054E352A6EE10A00AAA894 /* UISegmentRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISegmentRecorder.swift; sourceTree = ""; }; 61054E362A6EE10A00AAA894 /* UnsupportedViewRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsupportedViewRecorder.swift; sourceTree = ""; }; - 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ViewAttributes+Copy.swift"; sourceTree = ""; }; 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecordingContext.swift; sourceTree = ""; }; 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeIDGenerator.swift; sourceTree = ""; }; 61054E3A2A6EE10A00AAA894 /* WindowViewTreeSnapshotProducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowViewTreeSnapshotProducer.swift; sourceTree = ""; }; @@ -2277,7 +2276,7 @@ 61054F582A6EE1BA00AAA894 /* NodesFlattenerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesFlattenerTests.swift; sourceTree = ""; }; 61054F5A2A6EE1BA00AAA894 /* RecordingCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordingCoordinatorTests.swift; sourceTree = ""; }; 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+SessionReplayTests.swift"; sourceTree = ""; }; - 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+ContentFrameTests.swift"; sourceTree = ""; }; + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+SessionReplayTests.swift"; sourceTree = ""; }; 61054F612A6EE1BA00AAA894 /* WindowTouchSnapshotProducerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowTouchSnapshotProducerTests.swift; sourceTree = ""; }; 61054F632A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchIdentifierGeneratorTests.swift; sourceTree = ""; }; 61054F662A6EE1BA00AAA894 /* ViewTreeRecordingContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTreeRecordingContextTests.swift; sourceTree = ""; }; @@ -2978,6 +2977,7 @@ D26C49BE288982DA00802B2D /* FeatureUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureUpload.swift; sourceTree = ""; }; D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = ""; }; D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = ""; }; + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+SessionReplay.swift"; sourceTree = ""; }; D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = ""; }; D27CBD992BB5DBBB00C766AA /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; D284C73F2C2059F3005142CC /* ObjcExceptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionTests.swift; sourceTree = ""; }; @@ -3652,7 +3652,8 @@ 61054E152A6EE10A00AAA894 /* UIView+SessionReplay.swift */, 61054E162A6EE10A00AAA894 /* CFType+Safety.swift */, 61054E172A6EE10A00AAA894 /* SystemColors.swift */, - 61054E182A6EE10A00AAA894 /* CGRect+ContentFrame.swift */, + 61054E182A6EE10A00AAA894 /* CGRect+SessionReplay.swift */, + D274FD1B2CBFEF6D005270B5 /* CGSize+SessionReplay.swift */, ); name = Utilities; path = Recorder/Utilities; @@ -3694,7 +3695,6 @@ 61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */, 61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */, 61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */, - 61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */, 61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */, 61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */, 61054E272A6EE10A00AAA894 /* NodeRecorders */, @@ -3934,7 +3934,7 @@ isa = PBXGroup; children = ( 61054F5D2A6EE1BA00AAA894 /* UIView+SessionReplayTests.swift */, - 61054F5F2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift */, + 61054F5F2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift */, ); path = Utilties; sourceTree = ""; @@ -8399,7 +8399,6 @@ 962C41A72CA431370050B747 /* SessionReplayPrivacyOverrides.swift in Sources */, 61054E772A6EE10A00AAA894 /* ViewTreeRecorder.swift in Sources */, 61054E9E2A6EE10B00AAA894 /* Queue.swift in Sources */, - 61054E872A6EE10A00AAA894 /* ViewAttributes+Copy.swift in Sources */, 61054E6A2A6EE10A00AAA894 /* UIView+SessionReplay.swift in Sources */, 96F25A832CC7EA4400459567 /* UIView+SessionReplayPrivacyOverrides+objc.swift in Sources */, 61054E7D2A6EE10A00AAA894 /* UITextFieldRecorder.swift in Sources */, @@ -8416,7 +8415,8 @@ 61054E742A6EE10A00AAA894 /* ViewTreeSnapshotProducer.swift in Sources */, 61054E7E2A6EE10A00AAA894 /* NodeRecorder.swift in Sources */, 61054E6F2A6EE10A00AAA894 /* UIApplicationSwizzler.swift in Sources */, - 61054E6D2A6EE10A00AAA894 /* CGRect+ContentFrame.swift in Sources */, + 61054E6D2A6EE10A00AAA894 /* CGRect+SessionReplay.swift in Sources */, + D274FD1C2CBFEF6D005270B5 /* CGSize+SessionReplay.swift in Sources */, 61054E942A6EE10A00AAA894 /* TextObfuscator.swift in Sources */, A7B932FE2B1F6A0A00AE6477 /* SRDataModels+UIKit.swift in Sources */, 61054E862A6EE10A00AAA894 /* UnsupportedViewRecorder.swift in Sources */, @@ -8482,7 +8482,7 @@ 962C41A92CB00FD60050B747 /* DDSessionReplayTests.swift in Sources */, 61054FBD2A6EE1BA00AAA894 /* UIViewRecorderTests.swift in Sources */, 61054F952A6EE1BA00AAA894 /* SessionReplayConfigurationTests.swift in Sources */, - 61054FAC2A6EE1BA00AAA894 /* CGRect+ContentFrameTests.swift in Sources */, + 61054FAC2A6EE1BA00AAA894 /* CGRect+SessionReplayTests.swift in Sources */, 61054FC72A6EE1BA00AAA894 /* SRDataModelsMocks.swift in Sources */, 61054FC82A6EE1BA00AAA894 /* SnapshotProcessorSpy.swift in Sources */, A74A72872B10CE4100771FEB /* ResourceMocks.swift in Sources */, diff --git a/DatadogSessionReplay/Sources/Processor/Builders/RecordsBuilder.swift b/DatadogSessionReplay/Sources/Processor/Builders/RecordsBuilder.swift index b9bc276995..ed591e19b0 100644 --- a/DatadogSessionReplay/Sources/Processor/Builders/RecordsBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/Builders/RecordsBuilder.swift @@ -129,7 +129,7 @@ internal class RecordsBuilder { from snapshot: ViewTreeSnapshot, lastSnapshot: ViewTreeSnapshot ) -> SRRecord? { - guard lastSnapshot.viewportSize.aspectRatio != snapshot.viewportSize.aspectRatio else { + guard lastSnapshot.viewportSize.dd.aspectRatio != snapshot.viewportSize.dd.aspectRatio else { return nil } return .incrementalSnapshotRecord( diff --git a/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift index 0788c5bea3..d808722453 100644 --- a/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift @@ -67,7 +67,7 @@ extension SessionReplayWireframesBuilder { public func createShapeWireframe( id: WireframeID, frame: CGRect, - clip: SRContentClip? = nil, + clip: CGRect, borderColor: CGColor? = nil, borderWidth: CGFloat? = nil, backgroundColor: CGColor? = nil, @@ -76,7 +76,7 @@ extension SessionReplayWireframesBuilder { ) -> SRWireframe { let wireframe = SRShapeWireframe( border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth), - clip: clip, + clip: SRContentClip(frame, intersecting: clip), height: Int64(withNoOverflow: frame.height), id: id, shapeStyle: createShapeStyle(backgroundColor: backgroundColor, cornerRadius: cornerRadius, opacity: opacity), @@ -92,8 +92,8 @@ extension SessionReplayWireframesBuilder { id: WireframeID, resource: SessionReplayResource, frame: CGRect, + clip: CGRect, mimeType: String = "png", - clip: SRContentClip? = nil, borderColor: CGColor? = nil, borderWidth: CGFloat? = nil, backgroundColor: CGColor? = nil, @@ -106,7 +106,7 @@ extension SessionReplayWireframesBuilder { let wireframe = SRImageWireframe( base64: nil, // field deprecated - we should use resource endpoint instead border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth), - clip: clip, + clip: SRContentClip(frame, intersecting: clip), height: Int64(withNoOverflow: frame.height), id: id, isEmpty: false, // field deprecated - we should use placeholder wireframe instead @@ -123,10 +123,10 @@ extension SessionReplayWireframesBuilder { public func createTextWireframe( id: WireframeID, frame: CGRect, + clip: CGRect, text: String, textFrame: CGRect? = nil, textAlignment: SRTextPosition.Alignment? = nil, - clip: SRContentClip? = nil, textColor: CGColor? = nil, font: UIFont? = nil, fontOverride: FontOverride? = nil, @@ -167,7 +167,7 @@ extension SessionReplayWireframesBuilder { let wireframe = SRTextWireframe( border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth), - clip: clip, + clip: SRContentClip(frame, intersecting: clip), height: Int64(withNoOverflow: frame.height), id: id, shapeStyle: createShapeStyle(backgroundColor: backgroundColor, cornerRadius: cornerRadius, opacity: opacity), @@ -185,11 +185,11 @@ extension SessionReplayWireframesBuilder { public func createPlaceholderWireframe( id: Int64, frame: CGRect, - label: String, - clip: SRContentClip? = nil + clip: CGRect, + label: String ) -> SRWireframe { let wireframe = SRPlaceholderWireframe( - clip: clip, + clip: SRContentClip(frame, intersecting: clip), height: Int64(withNoOverflow: frame.size.height), id: id, label: label, @@ -203,7 +203,7 @@ extension SessionReplayWireframesBuilder { public func visibleWebViewWireframe( id: Int, frame: CGRect, - clip: SRContentClip? = nil, + clip: CGRect, borderColor: CGColor? = nil, borderWidth: CGFloat? = nil, backgroundColor: CGColor? = nil, @@ -212,7 +212,7 @@ extension SessionReplayWireframesBuilder { ) -> SRWireframe { let wireframe = SRWebviewWireframe( border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth), - clip: clip, + clip: SRContentClip(frame, intersecting: clip), height: Int64(withNoOverflow: frame.height), id: Int64(id), isVisible: true, @@ -281,11 +281,11 @@ internal typealias WireframesBuilder = SessionReplayWireframesBuilder // MARK: - Convenience internal extension WireframesBuilder { - func createShapeWireframe(id: WireframeID, frame: CGRect, attributes: ViewAttributes) -> SRWireframe { + func createShapeWireframe(id: WireframeID, attributes: ViewAttributes) -> SRWireframe { return createShapeWireframe( id: id, - frame: frame, - clip: nil, + frame: attributes.frame, + clip: attributes.clip, borderColor: attributes.layerBorderColor, borderWidth: attributes.layerBorderWidth, backgroundColor: attributes.backgroundColor, @@ -311,5 +311,55 @@ extension SRContentClip { top: top ) } + + /// Creates Content Clip by intersecting the frame with the clipping rectangle. + /// + /// If the clip rectangle does not intersect with the frame, Content Clip will be initialised with: + /// + /// SRContentClip( + /// bottom: nil, + /// left: frame.width, + /// right: nil, + /// top: frame.height + /// ) + /// + /// This will result in a wireframe with no drawing area. Recorders should, in practice, prevent + /// this use case. + /// + /// - Parameters: + /// - frame: The view frame. + /// - clip: The clipping rectangle. + init?(_ frame: CGRect, intersecting clip: CGRect) { + let intersection = frame.intersection(clip) + + if intersection.isEmpty { + self.init( + bottom: nil, + left: Int64(withNoOverflow: frame.width), + right: nil, + top: Int64(withNoOverflow: frame.height) + ) + + return + } + + let top = intersection.minY - frame.minY + let left = intersection.minX - frame.minX + let bottom = frame.maxY - intersection.maxY + let right = frame.maxX - intersection.maxX + + // more reliable than intersection == frame + if bottom.isZero, bottom.isZero, bottom.isZero, bottom.isZero { + return nil + } + + self.init( + bottom: bottom.isZero ? nil : Int64(withNoOverflow: bottom), + left: left.isZero ? nil : Int64(withNoOverflow: left), + right: right.isZero ? nil : Int64(withNoOverflow: right), + top: top.isZero ? nil : Int64(withNoOverflow: top) + ) + } } + #endif diff --git a/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift b/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift index d4e53e2545..96bf06e949 100644 --- a/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift +++ b/DatadogSessionReplay/Sources/Processor/Diffing/Diff+SRWireframes.swift @@ -76,7 +76,18 @@ internal protocol MutableWireframe { /// Syntactic sugar to return `new` value if it's different than `old`. private func use(_ new: V?, ifDifferentThan old: V?) -> V? { - return new == old ? nil : new + new == old ? nil : new +} + +/// Apply dedicated merge for Content Clip where we need to reset value to `0` when the value is **back** to `nil`. +/// That is because the player assumes no change when value is `nil`, but `nil` also means no clipping. +private func use(_ new: SRContentClip?, ifDifferentThan old: SRContentClip?) -> SRContentClip? { + new != old ? SRContentClip( + bottom: new?.bottom ?? old?.bottom.map { _ in 0 }, + left: new?.left ?? old?.left.map { _ in 0 }, + right: new?.right ?? old?.right.map { _ in 0 }, + top: new?.top ?? old?.top.map { _ in 0 } + ) : nil } extension SRWireframe: MutableWireframe { @@ -104,7 +115,6 @@ extension SRShapeWireframe: MutableWireframe { toType: type ) } - // print string of enum of otherWireframe guard other.id == id else { throw WireframeMutationError.idMismatch @@ -269,4 +279,5 @@ extension SRImageWireframe { hasher.combine(y) } } + #endif diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+ContentFrame.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+ContentFrame.swift deleted file mode 100644 index 74c2842a98..0000000000 --- a/DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+ContentFrame.swift +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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. - */ - -#if os(iOS) -import UIKit - -extension CGRect { - func contentFrame( - for contentSize: CGSize, - using contentMode: UIView.ContentMode - ) -> CGRect { - guard width > 0 && height > 0 && contentSize.width > 0 && contentSize.height > 0 else { - return .zero - } - let contentFrame: CGRect - switch contentMode { - case .scaleAspectFit: - let actualContentRect = self.size.scaleAspectFitRect(for: contentSize) - contentFrame = CGRect( - x: self.origin.x + actualContentRect.origin.x, - y: self.origin.y + actualContentRect.origin.y, - width: actualContentRect.size.width, - height: actualContentRect.size.height - ) - - case .scaleAspectFill: - let actualContentRect = self.size.scaleAspectFillRect(for: contentSize) - contentFrame = CGRect( - x: self.origin.x + actualContentRect.origin.x, - y: self.origin.y + actualContentRect.origin.y, - width: actualContentRect.size.width, - height: actualContentRect.size.height - ) - case .redraw, .center: - contentFrame = CGRect( - x: self.origin.x + (self.width - contentSize.width) / 2, - y: self.origin.y + (self.height - contentSize.height) / 2, - width: contentSize.width, - height: contentSize.height - ) - case .scaleToFill: - contentFrame = self - - case .topLeft: - contentFrame = CGRect( - x: self.origin.x, - y: self.origin.y, - width: contentSize.width, - height: contentSize.height - ) - case .topRight: - contentFrame = CGRect( - x: self.origin.x + (self.width - contentSize.width), - y: self.origin.y, - width: contentSize.width, - height: contentSize.height - ) - case .bottomLeft: - contentFrame = CGRect( - x: self.origin.x, - y: self.origin.y + (self.height - contentSize.height), - width: contentSize.width, - height: contentSize.height - ) - case .bottomRight: - contentFrame = CGRect( - x: self.origin.x + (self.width - contentSize.width), - y: self.origin.y + (self.height - contentSize.height), - width: contentSize.width, - height: contentSize.height - ) - case .top: - contentFrame = CGRect( - x: self.origin.x + (self.width - contentSize.width) / 2, - y: self.origin.y, - width: contentSize.width, - height: contentSize.height - ) - case .bottom: - contentFrame = CGRect( - x: self.origin.x + (self.width - contentSize.width) / 2, - y: self.origin.y + (self.height - contentSize.height), - width: contentSize.width, - height: contentSize.height - ) - case .left: - contentFrame = CGRect( - x: self.origin.x, - y: self.origin.y + (self.height - contentSize.height) / 2, - width: contentSize.width, - height: contentSize.height - ) - case .right: - contentFrame = CGRect( - x: self.origin.x + (self.width - contentSize.width), - y: self.origin.y + (self.height - contentSize.height) / 2, - width: contentSize.width, - height: contentSize.height - ) - - @unknown default: - contentFrame = self - } - return contentFrame - } -} - -extension CGSize { - var aspectRatio: CGFloat { - guard width > 0 else { - return 0 - } - return height / width - } -} - -fileprivate extension CGSize { - func scaleAspectFillRect(for contentSize: CGSize) -> CGRect { - let scale: CGFloat - if (contentSize.width - width) < (contentSize.height - height) { - scale = width / contentSize.width - } else { - scale = height / contentSize.height - } - let size = CGSize(width: contentSize.width * scale, height: contentSize.height * scale) - - return CGRect( - x: (width - size.width) / 2, - y: (height - size.height) / 2, - width: size.width, - height: size.height - ) - } - - func scaleAspectFitRect(for contentSize: CGSize) -> CGRect { - let imageAspectRatio = contentSize.height / contentSize.width - - var x, y, width, height: CGFloat - if imageAspectRatio > aspectRatio { - height = self.height - width = height / imageAspectRatio - x = (self.width / 2) - (width / 2) - y = 0 - } else { - width = self.width - height = width * imageAspectRatio - x = 0 - y = (self.height / 2) - (height / 2) - } - return CGRect(x: x, y: y, width: width, height: height) - } -} -#endif diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+SessionReplay.swift new file mode 100644 index 0000000000..7cb6af079a --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/CGRect+SessionReplay.swift @@ -0,0 +1,112 @@ +/* + * 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. + */ + +#if os(iOS) +import UIKit +import DatadogInternal + +extension CGRect: DatadogExtended {} + +internal extension DatadogExtension where ExtendedType == CGRect { + func contentFrame( + for contentSize: CGSize, + using contentMode: UIView.ContentMode + ) -> CGRect { + guard type.width > 0 && type.height > 0 && contentSize.width > 0 && contentSize.height > 0 else { + return .zero + } + switch contentMode { + case .scaleAspectFit: + let actualContentRect = type.size.dd.scaleAspectFitRect(for: contentSize) + return CGRect( + x: type.origin.x + actualContentRect.origin.x, + y: type.origin.y + actualContentRect.origin.y, + width: actualContentRect.size.width, + height: actualContentRect.size.height + ) + + case .scaleAspectFill: + let actualContentRect = type.size.dd.scaleAspectFillRect(for: contentSize) + return CGRect( + x: type.origin.x + actualContentRect.origin.x, + y: type.origin.y + actualContentRect.origin.y, + width: actualContentRect.size.width, + height: actualContentRect.size.height + ) + case .redraw, .center: + return CGRect( + x: type.origin.x + (type.width - contentSize.width) / 2, + y: type.origin.y + (type.height - contentSize.height) / 2, + width: contentSize.width, + height: contentSize.height + ) + case .scaleToFill: + return type + + case .topLeft: + return CGRect( + x: type.origin.x, + y: type.origin.y, + width: contentSize.width, + height: contentSize.height + ) + case .topRight: + return CGRect( + x: type.origin.x + (type.width - contentSize.width), + y: type.origin.y, + width: contentSize.width, + height: contentSize.height + ) + case .bottomLeft: + return CGRect( + x: type.origin.x, + y: type.origin.y + (type.height - contentSize.height), + width: contentSize.width, + height: contentSize.height + ) + case .bottomRight: + return CGRect( + x: type.origin.x + (type.width - contentSize.width), + y: type.origin.y + (type.height - contentSize.height), + width: contentSize.width, + height: contentSize.height + ) + case .top: + return CGRect( + x: type.origin.x + (type.width - contentSize.width) / 2, + y: type.origin.y, + width: contentSize.width, + height: contentSize.height + ) + case .bottom: + return CGRect( + x: type.origin.x + (type.width - contentSize.width) / 2, + y: type.origin.y + (type.height - contentSize.height), + width: contentSize.width, + height: contentSize.height + ) + case .left: + return CGRect( + x: type.origin.x, + y: type.origin.y + (type.height - contentSize.height) / 2, + width: contentSize.width, + height: contentSize.height + ) + case .right: + return CGRect( + x: type.origin.x + (type.width - contentSize.width), + y: type.origin.y + (type.height - contentSize.height) / 2, + width: contentSize.width, + height: contentSize.height + ) + + @unknown default: + return type + } + } +} + +#endif diff --git a/DatadogSessionReplay/Sources/Recorder/Utilities/CGSize+SessionReplay.swift b/DatadogSessionReplay/Sources/Recorder/Utilities/CGSize+SessionReplay.swift new file mode 100644 index 0000000000..a50dce69fd --- /dev/null +++ b/DatadogSessionReplay/Sources/Recorder/Utilities/CGSize+SessionReplay.swift @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#if os(iOS) +import UIKit +import DatadogInternal + +extension CGSize: DatadogExtended {} + +internal extension DatadogExtension where ExtendedType == CGSize { + var aspectRatio: CGFloat { + type.width > 0 ? type.height / type.width : 0 + } + + func scaleAspectFillRect(for contentSize: CGSize) -> CGRect { + let scale: CGFloat + if (contentSize.width - type.width) < (contentSize.height - type.height) { + scale = type.width / contentSize.width + } else { + scale = type.height / contentSize.height + } + let size = CGSize(width: contentSize.width * scale, height: contentSize.height * scale) + + return CGRect( + x: (type.width - size.width) / 2, + y: (type.height - size.height) / 2, + width: size.width, + height: size.height + ) + } + + func scaleAspectFitRect(for contentSize: CGSize) -> CGRect { + let imageAspectRatio = contentSize.height / contentSize.width + + var x, y, width, height: CGFloat + if imageAspectRatio > aspectRatio { + height = type.height + width = height / imageAspectRatio + x = (type.width / 2) - (width / 2) + y = 0 + } else { + width = type.width + height = width * imageAspectRatio + x = 0 + y = (type.height / 2) - (height / 2) + } + return CGRect(x: x, y: y, width: width, height: height) + } +} + +#endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIActivityIndicatorRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIActivityIndicatorRecorder.swift index ac151b2053..4a5ffe619c 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIActivityIndicatorRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIActivityIndicatorRecorder.swift @@ -64,6 +64,7 @@ internal struct UIActivityIndicatorWireframesBuilder: NodeWireframesBuilder { builder.createShapeWireframe( id: wireframeID, frame: wireframeRect, + clip: attributes.clip, backgroundColor: backgroundColor, cornerRadius: attributes.layerCornerRadius, opacity: attributes.alpha diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift index 44dd0e0e8a..4f90aebe73 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIDatePickerRecorder.swift @@ -179,7 +179,7 @@ internal struct UIDatePickerWireframesBuilder: NodeWireframesBuilder { builder.createShapeWireframe( id: backgroundWireframeID, frame: wireframeRect, - clip: nil, + clip: attributes.clip, borderColor: isDisplayedInPopover ? SystemColors.secondarySystemFill : nil, borderWidth: isDisplayedInPopover ? 1 : 0, backgroundColor: isDisplayedInPopover ? SystemColors.secondarySystemGroupedBackground : SystemColors.systemBackground, diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift index 2e05cd2a29..b4866485ee 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewRecorder.swift @@ -51,7 +51,7 @@ internal struct UIImageViewRecorder: NodeRecorder { let ids = context.ids.nodeIDs(2, view: imageView, nodeRecorder: self) let contentFrame = imageView.image.map { - attributes.frame.contentFrame( + attributes.frame.dd.contentFrame( for: $0.size, using: imageView.contentMode ) @@ -71,7 +71,6 @@ internal struct UIImageViewRecorder: NodeRecorder { imageWireframeID: ids[1], attributes: attributes, contentFrame: contentFrame, - clipsToBounds: imageView.clipsToBounds, imageResource: imageResource, imagePrivacyLevel: context.recorder.imagePrivacy ) @@ -96,40 +95,16 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { let contentFrame: CGRect? - let clipsToBounds: Bool - let imageResource: UIImageResource? let imagePrivacyLevel: ImagePrivacyLevel - private var clip: SRContentClip? { - guard let contentFrame = contentFrame else { - return nil - } - let top = max(relativeIntersectedRect.origin.y - contentFrame.origin.y, 0) - let left = max(relativeIntersectedRect.origin.x - contentFrame.origin.x, 0) - let bottom = max(contentFrame.height - (relativeIntersectedRect.height + top), 0) - let right = max(contentFrame.width - (relativeIntersectedRect.width + left), 0) - return SRContentClip( - bottom: Int64(withNoOverflow: bottom), - left: Int64(withNoOverflow: left), - right: Int64(withNoOverflow: right), - top: Int64(withNoOverflow: top) - ) - } - - private var relativeIntersectedRect: CGRect { - guard let contentFrame = contentFrame else { - return .zero - } - return attributes.frame.intersection(contentFrame) - } - func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { var wireframes = [ builder.createShapeWireframe( id: wireframeID, frame: attributes.frame, + clip: attributes.clip, borderColor: attributes.layerBorderColor, borderWidth: attributes.layerBorderWidth, backgroundColor: attributes.backgroundColor, @@ -148,14 +123,15 @@ internal struct UIImageViewWireframesBuilder: NodeWireframesBuilder { id: imageWireframeID, resource: imageResource, frame: contentFrame, - clip: clipsToBounds ? clip : nil + clip: attributes.clip ) ) } else { wireframes.append( builder.createPlaceholderWireframe( id: imageWireframeID, - frame: clipsToBounds ? relativeIntersectedRect : contentFrame, + frame: attributes.clip.intersection(contentFrame), // = visible frame of the image + clip: attributes.clip, label: imagePrivacyLevel == .maskNonBundledOnly ? "Content Image" : "Image" ) ) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift index a6f16ae961..fb8272adfe 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UILabelRecorder.swift @@ -78,6 +78,7 @@ internal struct UILabelWireframesBuilder: NodeWireframesBuilder { builder.createTextWireframe( id: wireframeID, frame: wireframeRect, + clip: attributes.clip, text: textObfuscator.mask(text: text), textAlignment: .init(systemTextAlignment: textAlignment), textColor: textColor, diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorder.swift index f41aadd9b2..d52646f9e4 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorder.swift @@ -78,6 +78,7 @@ internal struct UINavigationBarWireframesBuilder: NodeWireframesBuilder { builder.createShapeWireframe( id: wireframeID, frame: wireframeRect, + clip: attributes.clip, borderColor: UIColor.lightGray.withAlphaComponent(0.5).cgColor, borderWidth: 1, backgroundColor: color, diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift index 394635d579..f98667bf62 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIPickerViewRecorder.swift @@ -109,7 +109,7 @@ internal struct UIPickerViewWireframesBuilder: NodeWireframesBuilder { func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { return [ - builder.createShapeWireframe(id: backgroundWireframeID, frame: wireframeRect, attributes: attributes) + builder.createShapeWireframe(id: backgroundWireframeID, attributes: attributes) ] } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIProgressViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIProgressViewRecorder.swift index 69d28a2e64..d2c97b75e0 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIProgressViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIProgressViewRecorder.swift @@ -56,6 +56,7 @@ internal struct UIProgressViewWireframesBuilder: NodeWireframesBuilder { let background = builder.createShapeWireframe( id: backgroundWireframeID, frame: wireframeRect, + clip: attributes.clip, backgroundColor: backgroundColor ?? SystemColors.tertiarySystemFill, cornerRadius: wireframeRect.height / 2 ) @@ -68,8 +69,7 @@ internal struct UIProgressViewWireframesBuilder: NodeWireframesBuilder { let progressTrack = builder.createShapeWireframe( id: progressTrackWireframeID, frame: progressTrackFrame, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: progressTintColor, cornerRadius: wireframeRect.height / 2, opacity: attributes.alpha diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift index 160bb17580..d8dc513953 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISegmentRecorder.swift @@ -75,8 +75,7 @@ internal struct UISegmentWireframesBuilder: NodeWireframesBuilder { let background = builder.createShapeWireframe( id: backgroundWireframeID, frame: wireframeRect, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: attributes.backgroundColor ?? SystemColors.tertiarySystemFill, cornerRadius: 8, opacity: attributes.alpha @@ -103,6 +102,7 @@ internal struct UISegmentWireframesBuilder: NodeWireframesBuilder { return builder.createTextWireframe( id: segmentWireframeIDs[idx], frame: segmentRects[idx], + clip: attributes.clip, text: textObfuscator.mask(text: segmentTitles[idx] ?? ""), textFrame: segmentRects[idx], textAlignment: .init(horizontal: .center, vertical: .center), diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift index 8ea43a7ccd..8aefb51d10 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISliderRecorder.swift @@ -75,8 +75,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { let track = builder.createShapeWireframe( id: minTrackWireframeID, frame: trackFrame, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: SystemColors.tertiarySystemFill, cornerRadius: wireframeRect.height * 0.5, opacity: isEnabled ? attributes.alpha : 0.5 @@ -84,7 +83,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { // Create background wireframe if the underlying `UIView` has any appearance: if attributes.hasAnyAppearance { - let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: attributes.frame, attributes: attributes) + let background = builder.createShapeWireframe(id: backgroundWireframeID, attributes: attributes) return [background, track] } else { @@ -109,6 +108,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { let thumb = builder.createShapeWireframe( id: thumbWireframeID, frame: thumbFrame, + clip: attributes.clip, borderColor: isEnabled ? SystemColors.secondarySystemFill : SystemColors.tertiarySystemFill, borderWidth: 1, backgroundColor: isEnabled ? (thumbTintColor ?? UIColor.white.cgColor) : SystemColors.tertiarySystemBackground, @@ -124,8 +124,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { let leftTrack = builder.createShapeWireframe( id: minTrackWireframeID, frame: leftTrackFrame, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: minTrackTintColor ?? SystemColors.tintColor, cornerRadius: 0, opacity: isEnabled ? attributes.alpha : 0.5 @@ -139,8 +138,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { let rightTrack = builder.createShapeWireframe( id: maxTrackWireframeID, frame: rightTrackFrame, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: maxTrackTintColor ?? SystemColors.tertiarySystemFill, cornerRadius: 0, opacity: isEnabled ? attributes.alpha : 0.5 @@ -148,7 +146,7 @@ internal struct UISliderWireframesBuilder: NodeWireframesBuilder { if attributes.hasAnyAppearance { // Create background wireframe only if view declares visible background - let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: wireframeRect, attributes: attributes) + let background = builder.createShapeWireframe(id: backgroundWireframeID, attributes: attributes) return [background, leftTrack, rightTrack, thumb] } else { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift index 3f12ea2a33..33e139b425 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIStepperRecorder.swift @@ -28,6 +28,7 @@ internal struct UIStepperRecorder: NodeRecorder { let builder = UIStepperWireframesBuilder( wireframeRect: stepperFrame, + wireframeClip: attributes.clip, cornerRadius: stepper.subviews.first?.layer.cornerRadius ?? 0, backgroundWireframeID: ids[0], dividerWireframeID: ids[1], @@ -44,6 +45,7 @@ internal struct UIStepperRecorder: NodeRecorder { internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { let wireframeRect: CGRect + let wireframeClip: CGRect let cornerRadius: CGFloat let backgroundWireframeID: WireframeID let dividerWireframeID: WireframeID @@ -57,6 +59,7 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { let background = builder.createShapeWireframe( id: backgroundWireframeID, frame: wireframeRect, + clip: wireframeClip, borderColor: nil, borderWidth: nil, backgroundColor: SystemColors.tertiarySystemFill, @@ -69,6 +72,7 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { origin: CGPoint(x: 0, y: verticalMargin), size: CGSize(width: 1, height: wireframeRect.size.height - 2 * verticalMargin) ).putInside(wireframeRect, horizontalAlignment: .center, verticalAlignment: .middle), + clip: wireframeClip, backgroundColor: SystemColors.placeholderText ) @@ -78,18 +82,21 @@ internal struct UIStepperWireframesBuilder: NodeWireframesBuilder { let minus = builder.createShapeWireframe( id: minusWireframeID, frame: horizontalElementRect.putInside(leftButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), + clip: wireframeClip, backgroundColor: isMinusEnabled ? SystemColors.label : SystemColors.placeholderText, cornerRadius: horizontalElementRect.size.height ) let plusHorizontal = builder.createShapeWireframe( id: plusHorizontalWireframeID, frame: horizontalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), + clip: wireframeClip, backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, cornerRadius: horizontalElementRect.size.height ) let plusVertical = builder.createShapeWireframe( id: plusVerticalWireframeID, frame: verticalElementRect.putInside(rightButtonFrame, horizontalAlignment: .center, verticalAlignment: .middle), + clip: wireframeClip, backgroundColor: isPlusEnabled ? SystemColors.label : SystemColors.placeholderText, cornerRadius: verticalElementRect.size.width ) diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift index 414777351c..72ab34789f 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UISwitchRecorder.swift @@ -73,8 +73,7 @@ internal struct UISwitchWireframesBuilder: NodeWireframesBuilder { let track = builder.createShapeWireframe( id: trackWireframeID, frame: wireframeRect, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: SystemColors.tertiarySystemFill, cornerRadius: wireframeRect.height * 0.5, opacity: isEnabled ? attributes.alpha : 0.5 @@ -82,7 +81,7 @@ internal struct UISwitchWireframesBuilder: NodeWireframesBuilder { // Create background wireframe if the underlying `UIView` has any appearance: if attributes.hasAnyAppearance { - let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: attributes.frame, attributes: attributes) + let background = builder.createShapeWireframe(id: backgroundWireframeID, attributes: attributes) return [background, track] } else { @@ -98,8 +97,7 @@ internal struct UISwitchWireframesBuilder: NodeWireframesBuilder { let track = builder.createShapeWireframe( id: trackWireframeID, frame: wireframeRect, - borderColor: nil, - borderWidth: nil, + clip: attributes.clip, backgroundColor: trackColor, cornerRadius: radius, opacity: isEnabled ? attributes.alpha : 0.5 @@ -112,6 +110,7 @@ internal struct UISwitchWireframesBuilder: NodeWireframesBuilder { let thumb = builder.createShapeWireframe( id: thumbWireframeID, frame: thumbFrame, + clip: attributes.clip, borderColor: SystemColors.secondarySystemFill, borderWidth: 1, backgroundColor: thumbTintColor ?? ((isDarkMode && !isEnabled) ? UIColor.gray.cgColor : UIColor.white.cgColor), @@ -120,7 +119,7 @@ internal struct UISwitchWireframesBuilder: NodeWireframesBuilder { // Create background wireframe if the underlying `UIView` has any appearance: if attributes.hasAnyAppearance { - let background = builder.createShapeWireframe(id: backgroundWireframeID, frame: attributes.frame, attributes: attributes) + let background = builder.createShapeWireframe(id: backgroundWireframeID, attributes: attributes) return [background, track, thumb] } else { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift index 47c3e5cf2d..b026f78441 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorder.swift @@ -114,6 +114,7 @@ internal struct UITabBarWireframesBuilder: NodeWireframesBuilder { builder.createShapeWireframe( id: wireframeID, frame: wireframeRect, + clip: attributes.clip, borderColor: UIColor.lightGray.withAlphaComponent(0.5).cgColor, borderWidth: 0.5, backgroundColor: color, diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift index 45741dd97b..0128993220 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextFieldRecorder.swift @@ -122,6 +122,7 @@ internal struct UITextFieldWireframesBuilder: NodeWireframesBuilder { builder.createTextWireframe( id: wireframeID, frame: wireframeRect, + clip: attributes.clip, text: textObfuscator.mask(text: text), textFrame: wireframeRect, textAlignment: .init(systemTextAlignment: textAlignment), diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift index 2b4a5297e3..fd4be32e79 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITextViewRecorder.swift @@ -72,19 +72,6 @@ internal struct UITextViewWireframesBuilder: NodeWireframesBuilder { attributes.frame } - private var clip: SRContentClip { - let top = abs(contentRect.origin.y) - let left = abs(contentRect.origin.x) - let bottom = max(contentRect.height - attributes.frame.height - top, 0) - let right = max(contentRect.width - attributes.frame.width - left, 0) - return SRContentClip( - bottom: Int64(withNoOverflow: bottom), - left: Int64(withNoOverflow: left), - right: Int64(withNoOverflow: right), - top: Int64(withNoOverflow: top) - ) - } - private var relativeIntersectedRect: CGRect { // UITextView adds additional padding for presented content. let padding: CGFloat = 8 @@ -101,9 +88,9 @@ internal struct UITextViewWireframesBuilder: NodeWireframesBuilder { builder.createTextWireframe( id: wireframeID, frame: relativeIntersectedRect, + clip: attributes.clip, text: textObfuscator.mask(text: text), textAlignment: .init(systemTextAlignment: textAlignment, vertical: .top), - clip: clip, textColor: textColor, font: font, borderColor: attributes.layerBorderColor, diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift index 2a677184d4..bf83ee4814 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIViewRecorder.swift @@ -24,19 +24,18 @@ internal class UIViewRecorder: NodeRecorder { func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { var attributes = attributes if context.viewControllerContext.isRootView(of: .alert) { - attributes = attributes.copy { - $0.backgroundColor = SystemColors.systemBackground - $0.layerBorderColor = nil - $0.layerBorderWidth = 0 - $0.layerCornerRadius = 16 - $0.alpha = 1 - $0.isHidden = false - } + attributes.backgroundColor = SystemColors.systemBackground + attributes.layerBorderColor = nil + attributes.layerBorderWidth = 0 + attributes.layerCornerRadius = 16 + attributes.alpha = 1 + attributes.isHidden = false } guard attributes.isVisible else { return InvisibleElement.constant } + if let semantics = semanticsOverride(view, attributes) { return semantics } @@ -60,6 +59,7 @@ internal class UIViewRecorder: NodeRecorder { wireframeID: context.ids.nodeID(view: view, nodeRecorder: self), attributes: attributes ) + let node = Node(viewAttributes: attributes, wireframesBuilder: builder) return AmbiguousElement(nodes: [node]) } @@ -77,11 +77,20 @@ internal struct UIViewWireframesBuilder: NodeWireframesBuilder { func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { if attributes.overrides.hide == true { return [ - builder.createPlaceholderWireframe(id: wireframeID, frame: wireframeRect, label: "Hidden") + builder.createPlaceholderWireframe( + id: wireframeID, + frame: wireframeRect, + clip: attributes.clip, + label: "Hidden" + ) ] } + return [ - builder.createShapeWireframe(id: wireframeID, frame: wireframeRect, attributes: attributes) + builder.createShapeWireframe( + id: wireframeID, + attributes: attributes + ) ] } } diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift index d54a16b5d2..5a7bdb5609 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UnsupportedViewRecorder.swift @@ -54,6 +54,7 @@ internal struct UnsupportedViewWireframesBuilder: NodeWireframesBuilder { builder.createPlaceholderWireframe( id: wireframeID, frame: attributes.frame, + clip: attributes.clip, label: unsupportedClassName ) ] diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift index 85b8ea020e..b9bac38187 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/WKWebViewRecorder.swift @@ -48,6 +48,7 @@ internal struct WKWebViewWireframesBuilder: NodeWireframesBuilder { builder.visibleWebViewWireframe( id: slotID, frame: attributes.frame, + clip: attributes.clip, borderColor: attributes.layerBorderColor, borderWidth: attributes.layerBorderWidth, backgroundColor: attributes.backgroundColor, diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewAttributes+Copy.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewAttributes+Copy.swift deleted file mode 100644 index 7baba908d7..0000000000 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewAttributes+Copy.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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. - */ - -#if os(iOS) -import UIKit - -extension ViewAttributes { - /// struct copy, lets you overwrite specific variables retaining the value of the rest - /// using a closure to set the new values for the copy of the struct - func copy(_ build: (inout Builder) -> Void) -> ViewAttributes { - var builder = Builder(original: self) - build(&builder) - return builder.toViewAttributes() - } - - struct Builder { - var frame: CGRect - var backgroundColor: CGColor? - var layerBorderColor: CGColor? - var layerBorderWidth: CGFloat - var layerCornerRadius: CGFloat - var alpha: CGFloat - var isHidden: Bool - var intrinsicContentSize: CGSize - var overrides: PrivacyOverrides - - fileprivate init(original: ViewAttributes) { - self.frame = original.frame - self.backgroundColor = original.backgroundColor - self.layerBorderColor = original.layerBorderColor - self.layerBorderWidth = original.layerBorderWidth - self.layerCornerRadius = original.layerCornerRadius - self.alpha = original.alpha - self.isHidden = original.isHidden - self.intrinsicContentSize = original.intrinsicContentSize - self.overrides = original.overrides - } - - fileprivate func toViewAttributes() -> ViewAttributes { - return ViewAttributes( - frame: frame, - backgroundColor: backgroundColor, - layerBorderColor: layerBorderColor, - layerBorderWidth: layerBorderWidth, - layerCornerRadius: layerCornerRadius, - alpha: alpha, - isHidden: isHidden, - intrinsicContentSize: intrinsicContentSize, - overrides: overrides - ) - } - } -} -#endif diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift index a063196cee..8a6d046cf2 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorder.swift @@ -37,7 +37,17 @@ internal struct ViewTreeRecorder { context.viewControllerContext.isRootView = false } - let semantics = nodeSemantics(for: view, in: context, overrides: overrides) + // Convert the frame in root view space + let frame = view.convert(view.bounds, to: context.coordinateSpace) + + if view.clipsToBounds { + // Propagate view's clipping intersection when clipsToBounds is + // enabled. + context.clip = frame.intersection(context.clip) + } + + let attributes = ViewAttributes(view: view, frame: frame, clip: context.clip, overrides: overrides) + let semantics = nodeSemantics(for: view, with: attributes, in: context) if !semantics.nodes.isEmpty { nodes.append(contentsOf: semantics.nodes) @@ -54,13 +64,7 @@ internal struct ViewTreeRecorder { } } - private func nodeSemantics(for view: UIView, in context: ViewTreeRecordingContext, overrides: PrivacyOverrides) -> NodeSemantics { - let attributes = ViewAttributes( - frameInRootView: view.convert(view.bounds, to: context.coordinateSpace), - view: view, - overrides: overrides - ) - + private func nodeSemantics(for view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics { var semantics: NodeSemantics = UnknownElement.constant for nodeRecorder in nodeRecorders { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift index a98d1a0910..b3656e1e7b 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecordingContext.swift @@ -25,6 +25,8 @@ public struct SessionReplayViewTreeRecordingContext { var viewControllerContext: ViewControllerContext = .init() /// Webviews caching. let webViewCache: NSHashTable + /// The clipping rect to apply to wireframes. + var clip: CGRect } // This alias enables us to have a more unique name exposed through public-internal access level diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift index 17e9410480..da987e50c9 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshot.swift @@ -74,35 +74,71 @@ internal typealias Resource = SessionReplayResource @_spi(Internal) public struct SessionReplayViewAttributes: Equatable { /// The view's `frame`, in VTS's root view's coordinate space (usually, the screen coordinate space). - public let frame: CGRect + public internal(set) var frame: CGRect + + /// The view's clipping frame, in VTS's root view's coordinate space. + public internal(set) var clip: CGRect /// Original view's `.backgroundColor`. - public let backgroundColor: CGColor? + public internal(set) var backgroundColor: CGColor? /// Original view's `layer.borderColor`. - public let layerBorderColor: CGColor? + public internal(set) var layerBorderColor: CGColor? /// Original view's `layer.borderWidth`. - public let layerBorderWidth: CGFloat + public internal(set) var layerBorderWidth: CGFloat /// Original view's `layer.cornerRadius`. - public let layerCornerRadius: CGFloat + public internal(set) var layerCornerRadius: CGFloat /// Original view's `.alpha` (between `0.0` and `1.0`). - public let alpha: CGFloat + public internal(set) var alpha: CGFloat /// Original view's `.isHidden`. - let isHidden: Bool + var isHidden: Bool /// Original view's `.intrinsicContentSize`. - let intrinsicContentSize: CGSize + var intrinsicContentSize: CGSize + + /// If the view has privacy overrides, which take precedence over global masking privacy levels. + var overrides: PrivacyOverrides +} + +// This alias enables us to have a more unique name exposed through public-internal access level +internal typealias ViewAttributes = SessionReplayViewAttributes + +extension ViewAttributes { + /// Creates value-type view description. + /// + /// - Parameters: + /// - view: The view instance. + /// - frame: The view frame in root view's coordinate space. + /// - clip: The clipping frame in root view's coordinate space. + init(view: UIView, frame: CGRect, clip: CGRect, overrides: SessionReplayPrivacyOverrides) { + self.frame = frame + self.clip = clip + self.backgroundColor = view.backgroundColor?.cgColor.safeCast + self.layerBorderColor = view.layer.borderColor?.safeCast + self.layerBorderWidth = view.layer.borderWidth + self.layerCornerRadius = view.layer.cornerRadius + self.alpha = view.alpha + self.isHidden = view.isHidden + self.intrinsicContentSize = view.intrinsicContentSize + self.overrides = overrides + } /// If the view is technically visible (different than `!isHidden` because it also considers `alpha` and `frame != .zero`). - /// A view can be technically visible, but it may have no appearance in practise (e.g. if its colors use `0` alpha component). + /// A view can be technically visible, but it may have no appearance in practise (e.g. if its colors use `0` alpha component, or outside + /// of clipping frame). /// /// Example 1: A view is invisible if it has `.zero` size or it is fully transparent (`alpha == 0`). /// Example 2: A view can be visible if it has fully transparent background color, but its `alpha` is `0.5` or it occupies non-zero area. - var isVisible: Bool { !isHidden && alpha > 0 && frame != .zero } + var isVisible: Bool { + !isHidden && + alpha > 0 && + frame != .zero && + !frame.intersection(clip).isEmpty + } /// If the view has any visible appearance (considering: background color + border style). /// In other words: if this view brings anything visual. @@ -123,26 +159,6 @@ public struct SessionReplayViewAttributes: Equatable { /// Example 1: A view with blue background of alpha `0.5` is considered "translucent". /// Example 2: A view with blue semi-transparent background, but alpha `1` is also conisdered "translucent". var isTranslucent: Bool { !isVisible || alpha < 1 || backgroundColor?.alpha ?? 0 < 1 } - - /// If the view has privacy overrides, which take precedence over global masking privacy levels. - var overrides: PrivacyOverrides -} - -// This alias enables us to have a more unique name exposed through public-internal access level -internal typealias ViewAttributes = SessionReplayViewAttributes - -extension ViewAttributes { - init(frameInRootView: CGRect, view: UIView, overrides: SessionReplayPrivacyOverrides) { - self.frame = frameInRootView - self.backgroundColor = view.backgroundColor?.cgColor.safeCast - self.layerBorderColor = view.layer.borderColor?.safeCast - self.layerBorderWidth = view.layer.borderWidth - self.layerCornerRadius = view.layer.cornerRadius - self.alpha = view.alpha - self.isHidden = view.isHidden - self.intrinsicContentSize = view.intrinsicContentSize - self.overrides = overrides - } } extension ViewAttributes { diff --git a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift index 7d195a5db6..530403edd3 100644 --- a/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift +++ b/DatadogSessionReplay/Sources/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotBuilder.swift @@ -32,7 +32,8 @@ internal struct ViewTreeSnapshotBuilder { recorder: recorderContext, coordinateSpace: rootView, ids: idsGenerator, - webViewCache: webViewCache + webViewCache: webViewCache, + clip: rootView.bounds ) let nodes = viewTreeRecorder.record(rootView, in: context) let snapshot = ViewTreeSnapshot( diff --git a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift index 1649624184..fa4d9385ef 100644 --- a/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift +++ b/DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift @@ -56,8 +56,10 @@ extension ViewAttributes: AnyMockable, RandomMockable { /// Random mock, not guaranteeing consistency of returned `ViewAttributes`. public static func mockRandom() -> ViewAttributes { + let frame: CGRect = .mockRandom() return .init( - frame: .mockRandom(), + frame: frame, + clip: frame, backgroundColor: UIColor.mockRandom().cgColor, layerBorderColor: UIColor.mockRandom().cgColor, layerBorderWidth: .mockRandom(min: 0, max: 5), @@ -72,6 +74,7 @@ extension ViewAttributes: AnyMockable, RandomMockable { /// Partial mock, not guaranteeing consistency of returned `ViewAttributes`. static func mockWith( frame: CGRect = .mockAny(), + clip: CGRect = .mockAny(), backgroundColor: CGColor? = .mockAny(), layerBorderColor: CGColor? = .mockAny(), layerBorderWidth: CGFloat = .mockAny(), @@ -83,6 +86,7 @@ extension ViewAttributes: AnyMockable, RandomMockable { ) -> ViewAttributes { return .init( frame: frame, + clip: clip, backgroundColor: backgroundColor, layerBorderColor: layerBorderColor, layerBorderWidth: layerBorderWidth, @@ -119,12 +123,12 @@ extension ViewAttributes: AnyMockable, RandomMockable { /// Partial mock, guaranteeing consistency of returned `ViewAttributes`. static func mock(fixture: Fixture) -> ViewAttributes { - var frame: CGRect? + var frame: CGRect var backgroundColor: CGColor? var layerBorderColor: CGColor? var layerBorderWidth: CGFloat? - var alpha: CGFloat? - var isHidden: Bool? + var alpha: CGFloat + var isHidden: Bool // swiftlint:disable opening_brace switch fixture { @@ -136,7 +140,7 @@ extension ViewAttributes: AnyMockable, RandomMockable { // visible: isHidden = false alpha = .mockRandom(min: 0.1, max: 1) - frame = .mockRandom(minWidth: 10, minHeight: 10) + frame = .mockRandom(maxX: 5, maxY: 5, minWidth: 10, minHeight: 10) // no appearance: oneOrMoreOf([ { layerBorderWidth = 0 }, @@ -146,7 +150,7 @@ extension ViewAttributes: AnyMockable, RandomMockable { // visibile: isHidden = false alpha = .mockRandom(min: 0.1, max: 1) - frame = .mockRandom(minWidth: 10, minHeight: 10) + frame = .mockRandom(maxX: 5, maxY: 5, minWidth: 10, minHeight: 10) // some appearance: oneOrMoreOf([ { @@ -159,7 +163,7 @@ extension ViewAttributes: AnyMockable, RandomMockable { // opaque: isHidden = false alpha = 1 - frame = .mockRandom(minWidth: 10, minHeight: 10) + frame = .mockRandom(maxX: 5, maxY: 5, minWidth: 10, minHeight: 10) backgroundColor = UIColor.mockRandomWith(alpha: 1).cgColor layerBorderWidth = .mockRandom(min: 1, max: 5) layerBorderColor = UIColor.mockRandomWith(alpha: .mockRandom(min: 0.1, max: 1)).cgColor @@ -167,14 +171,15 @@ extension ViewAttributes: AnyMockable, RandomMockable { // swiftlint:enable opening_brace let mock = ViewAttributes( - frame: frame ?? .mockRandom(minWidth: 10, minHeight: 10), + frame: frame, + clip: frame, backgroundColor: backgroundColor, layerBorderColor: layerBorderColor, layerBorderWidth: layerBorderWidth ?? .mockRandom(min: 1, max: 4), layerCornerRadius: .mockRandom(min: 0, max: 4), - alpha: alpha ?? .mockRandom(min: 0.01, max: 1), - isHidden: isHidden ?? .mockRandom(), - intrinsicContentSize: (frame ?? .mockRandom(minWidth: 10, minHeight: 10)).size, + alpha: alpha, + isHidden: isHidden, + intrinsicContentSize: frame.size, overrides: .mockAny() ) @@ -233,7 +238,7 @@ struct ShapeWireframesBuilderMock: NodeWireframesBuilder { let wireframeRect: CGRect func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] { - return [builder.createShapeWireframe(id: .mockAny(), frame: wireframeRect)] + return [builder.createShapeWireframe(id: .mockAny(), frame: wireframeRect, clip: wireframeRect)] } } @@ -337,11 +342,13 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { } public static func mockRandom() -> ViewTreeRecordingContext { + let view = UIView.mockRandom() return .init( recorder: .mockRandom(), - coordinateSpace: UIView.mockRandom(), + coordinateSpace: view, ids: NodeIDGenerator(), - webViewCache: .weakObjects() + webViewCache: .weakObjects(), + clip: view.bounds ) } @@ -349,13 +356,15 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { recorder: Recorder.Context = .mockAny(), coordinateSpace: UICoordinateSpace = UIView.mockAny(), ids: NodeIDGenerator = NodeIDGenerator(), - webViewCache: NSHashTable = .weakObjects() + webViewCache: NSHashTable = .weakObjects(), + clip: CGRect? = nil ) -> ViewTreeRecordingContext { return .init( recorder: recorder, coordinateSpace: coordinateSpace, ids: ids, - webViewCache: webViewCache + webViewCache: webViewCache, + clip: clip ?? coordinateSpace.bounds ) } } @@ -363,6 +372,8 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable { class NodeRecorderMock: NodeRecorder { var identifier = UUID() var queriedViews: Set = [] + var queryAttributes: [ViewAttributes] = [] + var queryAttributesByView: [UIView: ViewAttributes] = [:] var queryContexts: [ViewTreeRecordingContext] = [] var queryContextsByView: [UIView: ViewTreeRecordingContext] = [:] var resultForView: ((UIView) -> NodeSemantics?)? @@ -373,6 +384,8 @@ class NodeRecorderMock: NodeRecorder { func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? { queriedViews.insert(view) + queryAttributes.append(attributes) + queryAttributesByView[view] = attributes queryContexts.append(context) queryContextsByView[view] = context return resultForView?(view) @@ -533,7 +546,7 @@ internal func mockUIView(with attributes: ViewAttributes) -> View // Consistency check - to make sure computed properties in `ViewAttributes` captured // for mocked view are equal the these from requested `attributes`. let expectedAttributes = attributes - let actualAttributes = ViewAttributes(frameInRootView: view.frame, view: view, overrides: .mockAny()) + let actualAttributes = ViewAttributes(view: view, frame: view.frame, clip: view.frame, overrides: .mockAny()) assert( actualAttributes.isVisible == expectedAttributes.isVisible, diff --git a/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift index 5e7210f586..c0ffac243b 100644 --- a/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Builders/WireframesBuilderTests.swift @@ -18,7 +18,7 @@ class WireframesBuilderTests: XCTestCase { slots.forEach { id in let frame: CGRect = .mockRandom() - let wireframe = builder.visibleWebViewWireframe(id: id, frame: frame) + let wireframe = builder.visibleWebViewWireframe(id: id, frame: frame, clip: frame) guard case let .webviewWireframe(wireframe) = wireframe else { return XCTFail("The wireframe must be webviewWireframe case") } @@ -65,7 +65,7 @@ class WireframesBuilderTests: XCTestCase { let id: WireframeID = .mockRandom() let resource: MockResource = .mockRandom() let frame: CGRect = .mockRandom() - let clip: SRContentClip = .mockRandom() + let clip = frame.insetBy(dx: 1, dy: 1) let builder = WireframesBuilder() let wireframe = builder.createImageWireframe( @@ -81,7 +81,7 @@ class WireframesBuilderTests: XCTestCase { XCTAssertEqual(wireframe.id, id) XCTAssertNil(wireframe.border) - XCTAssertEqual(wireframe.clip, clip) + XCTAssertEqual(wireframe.clip, .init(bottom: 1, left: 1, right: 1, top: 1)) XCTAssertEqual(wireframe.height, Int64(withNoOverflow: frame.height)) XCTAssertNil(wireframe.shapeStyle) XCTAssertEqual(wireframe.width, Int64(withNoOverflow: frame.width)) @@ -90,5 +90,47 @@ class WireframesBuilderTests: XCTestCase { XCTAssertEqual(builder.resources.first?.calculateIdentifier(), resource.identifier) XCTAssertEqual(builder.resources.first?.calculateData(), resource.data) } + + func testContentClip_fromIntersection() { + let frame: CGRect = .mockRandom(minWidth: 21, minHeight: 21) + + // Inner clip + let clip = SRContentClip( + frame, + intersecting: frame.insetBy(dx: 10, dy: 10) + ) + + XCTAssertEqual(clip?.top, 10) + XCTAssertEqual(clip?.left, 10) + XCTAssertEqual(clip?.bottom, 10) + XCTAssertEqual(clip?.right, 10) + } + + func testContentClip_whenIntersection_isEqualToFrame() { + let frame: CGRect = .mockRandom() + + // Intersectin is equal to frame + let clip = SRContentClip( + frame, + intersecting: frame.insetBy(dx: -10, dy: -10) + ) + + XCTAssertNil(clip) + } + + func testContentClip_fromNoRectIntersection() { + let frame: CGRect = .mockRandom() + + // Not intersecting clip + let clip = SRContentClip( + frame, + intersecting: frame.offsetBy(dx: frame.width, dy: frame.height) + ) + + XCTAssertEqual(clip?.top, Int64(withNoOverflow: frame.height)) + XCTAssertEqual(clip?.left, Int64(withNoOverflow: frame.width)) + XCTAssertNil(clip?.bottom) + XCTAssertNil(clip?.right) + } } #endif diff --git a/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift b/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift index 6dc326c4d9..91b74592e6 100644 --- a/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift +++ b/DatadogSessionReplay/Tests/Processor/Diffing/Diff+SRWireframesTests.swift @@ -136,7 +136,7 @@ class DiffSRWireframes: XCTestCase { let wireframeB: SRWireframe = .imageWireframe(value: .mockWith(base64: base64, id: randomID)) // When - let mutations = try? XCTUnwrap(wireframeB.mutations(from: wireframeA)) + let mutations = try? wireframeB.mutations(from: wireframeA) let isDifferent = wireframeA.isDifferent(than: wireframeB) // Then @@ -147,6 +147,39 @@ class DiffSRWireframes: XCTestCase { XCTFail("mutations are expected to be `.imageWireframeUpdate`") } } + + func testUpdatingContentClip() throws { + let id: WireframeID = .mockRandom() + + // Given + let clip0 = SRContentClip(bottom: nil, left: nil, right: nil, top: nil) + let wireframe0: SRWireframe = .shapeWireframe(value: .mockWith(clip: clip0, id: id)) + + let clip1 = SRContentClip(bottom: nil, left: nil, right: 10, top: 10) + let wireframe1: SRWireframe = .shapeWireframe(value: .mockWith(clip: clip1, id: id)) + + let clip2 = SRContentClip(bottom: 10, left: 10, right: nil, top: nil) + let wireframe2: SRWireframe = .shapeWireframe(value: .mockWith(clip: clip2, id: id)) + + // When + let mutation1 = try XCTUnwrap(wireframe1.mutations(from: wireframe0)) + let result1 = try XCTUnwrap(wireframe0.merge(mutation: mutation1)?.shapeWireframe) + + let mutation2 = try XCTUnwrap(wireframe2.mutations(from: wireframe1)) + let result2 = try XCTUnwrap(wireframe1.merge(mutation: mutation2)?.shapeWireframe) + + let mutation3 = try XCTUnwrap(wireframe0.mutations(from: wireframe2)) + let result3 = try XCTUnwrap(wireframe2.merge(mutation: mutation3)?.shapeWireframe) + + let mutation4 = try XCTUnwrap(wireframe0.mutations(from: wireframe0)) + let result4 = try XCTUnwrap(wireframe0.merge(mutation: mutation4)?.shapeWireframe) + + // Then + XCTAssertEqual(result1.clip, SRContentClip(bottom: nil, left: nil, right: 10, top: 10)) + XCTAssertEqual(result2.clip, SRContentClip(bottom: 10, left: 10, right: 0, top: 0)) + XCTAssertEqual(result3.clip, SRContentClip(bottom: 0, left: 0, right: nil, top: nil)) + XCTAssertEqual(result4.clip, SRContentClip(bottom: nil, left: nil, right: nil, top: nil)) + } } // MARK: - Helpers @@ -228,5 +261,13 @@ extension SRWireframe { y: update.y ?? wireframe.y ) } + + fileprivate var shapeWireframe: SRShapeWireframe? { + guard case let .shapeWireframe(wireframe) = self else { + return nil + } + + return wireframe + } } #endif diff --git a/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift b/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift index b8b313b76c..321692a215 100644 --- a/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift +++ b/DatadogSessionReplay/Tests/Processor/SnapshotProcessorTests.swift @@ -311,7 +311,6 @@ class SnapshotProcessorTests: XCTestCase { imageWireframeID: .mockAny(), attributes: .mockAny(), contentFrame: .mockAny(), - clipsToBounds: .mockAny(), imageResource: resource, imagePrivacyLevel: .maskNonBundledOnly ) diff --git a/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift b/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+SessionReplayTests.swift similarity index 68% rename from DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift rename to DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+SessionReplayTests.swift index 02d1c60c54..46a48d2265 100644 --- a/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+ContentFrameTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/Utilties/CGRect+SessionReplayTests.swift @@ -9,44 +9,44 @@ import XCTest import UIKit @testable import DatadogSessionReplay -class CGRectContentFrameTests: XCTestCase { +class CGRectSessionReplayTests: XCTestCase { let accuracy = CGFloat(0.001) func testZeroContentFrame() { XCTAssertRectsEqual( - CGRect.zero.contentFrame(for: CGSize.mockAny(), using: .mockRandom()), + CGRect.zero.dd.contentFrame(for: CGSize.mockAny(), using: .mockRandom()), .zero, accuracy: accuracy ) XCTAssertRectsEqual( CGRect(origin: .zero, size: CGSize(width: 0, height: 100)) - .contentFrame(for: CGSize.mockAny(), using: .mockRandom()), + .dd.contentFrame(for: CGSize.mockAny(), using: .mockRandom()), .zero, accuracy: accuracy ) XCTAssertRectsEqual( CGRect(origin: .zero, size: CGSize(width: 100, height: 0)) - .contentFrame(for: CGSize.zero, using: .mockRandom()), + .dd.contentFrame(for: CGSize.zero, using: .mockRandom()), .zero, accuracy: accuracy ) XCTAssertRectsEqual( - CGRect.mockAny().contentFrame(for: CGSize.zero, using: .mockRandom()), + CGRect.mockAny().dd.contentFrame(for: CGSize.zero, using: .mockRandom()), .zero, accuracy: accuracy ) XCTAssertRectsEqual( - CGRect.mockAny().contentFrame(for: CGSize(width: 0, height: 100), using: .mockRandom()), + CGRect.mockAny().dd.contentFrame(for: CGSize(width: 0, height: 100), using: .mockRandom()), .zero, accuracy: accuracy ) XCTAssertRectsEqual( - CGRect.mockAny().contentFrame(for: CGSize(width: 100, height: 0), using: .mockRandom()), + CGRect.mockAny().dd.contentFrame(for: CGSize(width: 100, height: 0), using: .mockRandom()), .zero, accuracy: accuracy ) @@ -56,67 +56,67 @@ class CGRectContentFrameTests: XCTestCase { let frame = CGRect(x: 10, y: 10, width: 100, height: 100) let contentSize = CGSize(width: 21, height: 19.5) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .scaleAspectFit), + frame.dd.contentFrame(for: contentSize, using: .scaleAspectFit), CGRect(x: 10.0, y: 13.57142857142857, width: 100.0, height: 92.85714285714286), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .scaleAspectFill), + frame.dd.contentFrame(for: contentSize, using: .scaleAspectFill), CGRect(x: 6.153846153846146, y: 9.999999999999993, width: 107.69230769230771, height: 100.00000000000001), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .scaleToFill), + frame.dd.contentFrame(for: contentSize, using: .scaleToFill), CGRect(x: 10, y: 10, width: 100, height: 100), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .redraw), + frame.dd.contentFrame(for: contentSize, using: .redraw), CGRect(x: 49.5, y: 50.25, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .center), + frame.dd.contentFrame(for: contentSize, using: .center), CGRect(x: 49.5, y: 50.25, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .left), + frame.dd.contentFrame(for: contentSize, using: .left), CGRect(x: 10.0, y: 50.25, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .right), + frame.dd.contentFrame(for: contentSize, using: .right), CGRect(x: 89.0, y: 50.25, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .top), + frame.dd.contentFrame(for: contentSize, using: .top), CGRect(x: 49.5, y: 10.0, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .bottom), + frame.dd.contentFrame(for: contentSize, using: .bottom), CGRect(x: 49.5, y: 90.5, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .bottomLeft), + frame.dd.contentFrame(for: contentSize, using: .bottomLeft), CGRect(x: 10.0, y: 90.5, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .bottomRight), + frame.dd.contentFrame(for: contentSize, using: .bottomRight), CGRect(x: 89.0, y: 90.5, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .topLeft), + frame.dd.contentFrame(for: contentSize, using: .topLeft), CGRect(x: 10.0, y: 10.0, width: 21.0, height: 19.5), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .topRight), + frame.dd.contentFrame(for: contentSize, using: .topRight), CGRect(x: 89.0, y: 10.0, width: 21.0, height: 19.5), accuracy: accuracy ) @@ -126,67 +126,67 @@ class CGRectContentFrameTests: XCTestCase { let frame = CGRect(x: 100, y: 100, width: 100, height: 100) let contentSize = CGSize(width: 200, height: 200) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .scaleAspectFit), + frame.dd.contentFrame(for: contentSize, using: .scaleAspectFit), CGRect(x: 100.0, y: 100.0, width: 100.0, height: 100.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .scaleAspectFill), + frame.dd.contentFrame(for: contentSize, using: .scaleAspectFill), CGRect(x: 100.0, y: 100.0, width: 100.0, height: 100.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .scaleToFill), + frame.dd.contentFrame(for: contentSize, using: .scaleToFill), CGRect(x: 100.0, y: 100.0, width: 100.0, height: 100.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .redraw), + frame.dd.contentFrame(for: contentSize, using: .redraw), CGRect(x: 50.0, y: 50.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .center), + frame.dd.contentFrame(for: contentSize, using: .center), CGRect(x: 50.0, y: 50.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .left), + frame.dd.contentFrame(for: contentSize, using: .left), CGRect(x: 100.0, y: 50.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .right), + frame.dd.contentFrame(for: contentSize, using: .right), CGRect(x: 0.0, y: 50.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .top), + frame.dd.contentFrame(for: contentSize, using: .top), CGRect(x: 50.0, y: 100.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .bottom), + frame.dd.contentFrame(for: contentSize, using: .bottom), CGRect(x: 50.0, y: 0.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .bottomLeft), + frame.dd.contentFrame(for: contentSize, using: .bottomLeft), CGRect(x: 100.0, y: 0.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .bottomRight), + frame.dd.contentFrame(for: contentSize, using: .bottomRight), CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .topLeft), + frame.dd.contentFrame(for: contentSize, using: .topLeft), CGRect(x: 100.0, y: 100.0, width: 200.0, height: 200.0), accuracy: accuracy ) XCTAssertRectsEqual( - frame.contentFrame(for: contentSize, using: .topRight), + frame.dd.contentFrame(for: contentSize, using: .topRight), CGRect(x: 0.0, y: 100.0, width: 200.0, height: 200.0), accuracy: accuracy ) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift index f117e71e16..ca3fd9ba16 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UIImageViewWireframesBuilderTests.swift @@ -25,7 +25,6 @@ class UIImageViewWireframesBuilderTests: XCTestCase { imageWireframeID: imageWireframeID, attributes: ViewAttributes.mock(fixture: .visible(.someAppearance)), contentFrame: CGRect(x: 10, y: 10, width: 200, height: 200), - clipsToBounds: true, imageResource: .mockRandom(), imagePrivacyLevel: .maskNonBundledOnly ) @@ -56,7 +55,6 @@ class UIImageViewWireframesBuilderTests: XCTestCase { imageWireframeID: placeholderWireframeID, attributes: ViewAttributes.mock(fixture: .visible(.someAppearance)), contentFrame: CGRect(x: 10, y: 10, width: 200, height: 200), - clipsToBounds: true, imageResource: nil, imagePrivacyLevel: .maskNonBundledOnly ) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift index 0b890914fb..888a52d3e2 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UINavigationBarRecorderTests.swift @@ -21,7 +21,7 @@ class UINavigationBarRecorderTests: XCTestCase { ] let navigationBar = UINavigationBar.mock(withFixture: fixtures.randomElement()!) - let viewAttributes = ViewAttributes(frameInRootView: navigationBar.frame, view: navigationBar, overrides: .mockAny()) + let viewAttributes = ViewAttributes(view: navigationBar, frame: navigationBar.frame, clip: navigationBar.frame, overrides: .mockAny()) // When let semantics = try XCTUnwrap(recorder.semantics(of: navigationBar, with: viewAttributes, in: .mockAny()) as? SpecificElement) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift index 41b6dd7b6a..4aeb4364e1 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/NodeRecorders/UITabBarRecorderTests.swift @@ -17,7 +17,7 @@ class UITabBarRecorderTests: XCTestCase { func testWhenViewIsOfExpectedType() throws { // When let tabBar = UITabBar.mock(withFixture: .allCases.randomElement()!) - let viewAttributes = ViewAttributes(frameInRootView: tabBar.frame, view: tabBar, overrides: .mockAny()) + let viewAttributes = ViewAttributes(view: tabBar, frame: tabBar.frame, clip: tabBar.frame, overrides: .mockAny()) // Then let semantics = try XCTUnwrap(recorder.semantics(of: tabBar, with: viewAttributes, in: .mockAny())) @@ -38,7 +38,7 @@ class UITabBarRecorderTests: XCTestCase { // Given let tabBar = UITabBar.mock(withFixture: .visible(.someAppearance)) tabBar.items = [UITabBarItem(title: "first", image: UIImage(), tag: 0)] - let viewAttributes = ViewAttributes(frameInRootView: tabBar.frame, view: tabBar, overrides: .mockAny()) + let viewAttributes = ViewAttributes(view: tabBar, frame: tabBar.frame, clip: tabBar.frame, overrides: .mockAny()) // When let semantics1 = recorder.semantics(of: tabBar, with: viewAttributes, in: .mockAny()) diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift index 5dd34b2ad6..a45e6f3100 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeRecorderTests.swift @@ -182,19 +182,19 @@ class ViewTreeRecorderTests: XCTestCase { let `switch` = UISwitch.mock(withFixture: .visible(.noAppearance)) // When - var nodes = recorder.record(view, in: .mockRandom()) + var nodes = recorder.record(view, in: .mockWith(clip: view.frame)) XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UIView` when it has no appearance") - nodes = recorder.record(label, in: .mockRandom()) + nodes = recorder.record(label, in: .mockWith(clip: label.frame)) XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UILabel` when it has no appearance") - nodes = recorder.record(imageView, in: .mockRandom()) + nodes = recorder.record(imageView, in: .mockWith(clip: imageView.frame)) XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UIImageView` when it has no appearance") - nodes = recorder.record(textField, in: .mockRandom()) + nodes = recorder.record(textField, in: .mockWith(clip: textField.frame)) XCTAssertTrue(nodes.isEmpty, "No nodes should be recorded for `UITextField` when it has no appearance") - nodes = recorder.record(`switch`, in: .mockRandom()) + nodes = recorder.record(`switch`, in: .mockWith(clip: `switch`.frame)) XCTAssertFalse( nodes.isEmpty, "`UISwitch` with no appearance should record some nodes as it has style coming from its internal subtree." @@ -216,7 +216,7 @@ class ViewTreeRecorderTests: XCTestCase { views.forEach { view in // When - let nodes = recorder.record(view, in: .mockRandom()) + let nodes = recorder.record(view, in: .mockWith(clip: view.frame)) // Then XCTAssertFalse(nodes.isEmpty, "Some nodes should be recorded for \(type(of: view)) when it has some appearance") } @@ -306,7 +306,7 @@ class ViewTreeRecorderTests: XCTestCase { parentView.dd.sessionReplayPrivacyOverrides.hide = true // When - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 1) @@ -322,7 +322,7 @@ class ViewTreeRecorderTests: XCTestCase { parentView.dd.sessionReplayPrivacyOverrides.hide = false // When - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 2) @@ -337,7 +337,7 @@ class ViewTreeRecorderTests: XCTestCase { // When let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 2) @@ -361,8 +361,8 @@ class ViewTreeRecorderTests: XCTestCase { XCTAssertEqual(nodes.count, 1) let node = nodes.first XCTAssertNotNil(node) - XCTAssertEqual(node!.viewAttributes.overrides.imagePrivacy, viewImagePrivacy) - let builder = node!.wireframesBuilder as? UIImageViewWireframesBuilder + XCTAssertEqual(node?.viewAttributes.overrides.imagePrivacy, viewImagePrivacy) + let builder = node?.wireframesBuilder as? UIImageViewWireframesBuilder XCTAssertNotNil(builder) XCTAssertNotNil(builder!.imageResource) } @@ -380,7 +380,7 @@ class ViewTreeRecorderTests: XCTestCase { // When let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 2) @@ -404,7 +404,7 @@ class ViewTreeRecorderTests: XCTestCase { // When let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 2) @@ -414,5 +414,61 @@ class ViewTreeRecorderTests: XCTestCase { XCTAssertNotNil(builder) XCTAssertNil(builder!.imageResource) } + + func testItPropagateClippingIntersection() { + let nodeRecorder = NodeRecorderMock(resultForView: { _ in + MockSemantics(subtreeStrategy: .record, nodeNames: [], resourcesIdentifiers: []) + }) + let recorder = ViewTreeRecorder(nodeRecorders: [nodeRecorder]) + + // ┌──────────────┐ + // │Root │ + // │ ┌────────┐ │ + // │ │ View1 │ │ + // │ │ ┌────────┐│ + // │ │ │ View2│ ││ + // │ │ │ │ ││ + // │ │ │ │ ││ + // │ │ │ │ ││ + // │ └─│──────┘ ││ + // │ └────────┘│ + // └──────────────┘ + + // Given + var frame = CGRect(origin: .zero, size: .mockRandom(minWidth: 21, minHeight: 21)) + let rootView = UIView(frame: frame) + + frame = frame.insetBy(dx: 10, dy: 10) + let view1 = UIView(frame: frame) + view1.clipsToBounds = true // clip to the first view + rootView.addSubview(view1) + + frame = frame.offsetBy(dx: 10, dy: 10) + let view2 = UIView(frame: frame) + view1.addSubview(view2) + + // When + _ = recorder.record(rootView, in: .mockWith(coordinateSpace: rootView)) + + // Then + var context = nodeRecorder.queryContextsByView[rootView] + var attributes = nodeRecorder.queryAttributesByView[rootView] + // clip to the root view + XCTAssertEqual(context?.clip, rootView.bounds) + XCTAssertEqual(attributes?.clip, rootView.bounds) + + context = nodeRecorder.queryContextsByView[view1] + attributes = nodeRecorder.queryAttributesByView[view1] + // clip to view 1 + XCTAssertEqual(context?.clip, view1.frame) + XCTAssertEqual(attributes?.clip, view1.frame) + + context = nodeRecorder.queryContextsByView[view2] + attributes = nodeRecorder.queryAttributesByView[view2] + // still clip to view 1 + XCTAssertEqual(context?.clip, view1.frame) + XCTAssertEqual(attributes?.clip, view1.frame) + } } + #endif diff --git a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift index e4b4ae96ed..6d97b84932 100644 --- a/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift +++ b/DatadogSessionReplay/Tests/Recorder/ViewTreeSnapshotProducer/ViewTreeSnapshot/ViewTreeSnapshotTests.swift @@ -22,6 +22,7 @@ class ViewAttributesTests: XCTestCase { // Then XCTAssertEqual(attributes.frame, view.frame) + XCTAssertEqual(attributes.clip, view.frame) XCTAssertEqual(attributes.backgroundColor, view.backgroundColor?.cgColor) XCTAssertEqual(attributes.layerBorderColor, view.layer.borderColor) XCTAssertEqual(attributes.layerBorderWidth, view.layer.borderWidth) @@ -43,25 +44,28 @@ class ViewAttributesTests: XCTestCase { view.isHidden = false view.alpha = .mockRandom(min: 0.01, max: 1.0) view.frame = .mockRandom(minWidth: 0.01, minHeight: 0.01) + let clip = view.frame.insetBy(dx: 1, dy: 1) + let attributes = createViewAttributes(with: view, clip: clip) // Then - let attributes = createViewAttributes(with: view) XCTAssertTrue(attributes.isVisible) } func testWhenViewIsNotVisible() { // Given let view: UIView = .mockRandom() + var clip = view.frame // When oneOrMoreOf([ { view.isHidden = true }, { view.alpha = 0 }, { view.frame = .zero }, + { clip = clip.offsetBy(dx: clip.width, dy: clip.height) }, ]) // Then - let attributes = createViewAttributes(with: view) + let attributes = createViewAttributes(with: view, clip: clip) XCTAssertFalse(attributes.isVisible) } @@ -78,9 +82,7 @@ class ViewAttributesTests: XCTestCase { view.layer.borderWidth = .mockRandom(min: 0.01, max: 10) view.layer.borderColor = UIColor.mockRandomWith(alpha: .mockRandom(min: 0.01, max: 1)).cgColor }, - { - view.backgroundColor = .mockRandomWith(alpha: .mockRandom(min: 0.01, max: 1)) - } + { view.backgroundColor = .mockRandomWith(alpha: .mockRandom(min: 0.01, max: 1)) } ]) // Then @@ -91,40 +93,42 @@ class ViewAttributesTests: XCTestCase { func testWhenViewHasNoAppearance() { // Given let view: UIView = .mockRandom() + var clip = view.frame // When oneOf([ - { - view.isHidden = false - view.alpha = 0 - view.frame = .zero - }, + { view.isHidden = true }, + { view.alpha = 0 }, + { view.frame = .zero }, + { clip = clip.offsetBy(dx: clip.width, dy: clip.height) }, { view.layer.borderWidth = 0 view.layer.borderColor = UIColor.mockRandomWith(alpha: 0).cgColor - view.backgroundColor = .mockRandomWith(alpha: 0) - } + }, + { view.backgroundColor = .mockRandomWith(alpha: 0) } ]) // Then - let attributes = createViewAttributes(with: view) + let attributes = createViewAttributes(with: view, clip: clip) XCTAssertFalse(attributes.hasAnyAppearance) } func testWhenViewIsTranslucent() { // Given let view: UIView = .mockRandom() + var clip = view.frame // When oneOrMoreOf([ { view.isHidden = true }, { view.alpha = .mockRandom(min: 0, max: 0.99) }, { view.frame = .zero }, + { clip = clip.offsetBy(dx: clip.width, dy: clip.height) }, { view.backgroundColor = .mockRandomWith(alpha: .mockRandom(min: 0, max: 0.99)) } ]) // Then - let attributes = createViewAttributes(with: view) + let attributes = createViewAttributes(with: view, clip: clip) XCTAssertTrue(attributes.isTranslucent) } @@ -155,35 +159,6 @@ class ViewAttributesTests: XCTestCase { XCTAssertNil(attributes.layerBorderColor) } - func testWhenCopy() { - let view: UIView = .mockRandom() - let rect: CGRect = .mockRandom() - let color: CGColor = .mockRandom() - let float: CGFloat = .mockRandom() - let boolean: Bool = .mockRandom() - let overrides: PrivacyOverrides = .mockRandom() - let attributes = ViewAttributes(frameInRootView: view.frame, view: view, overrides: overrides).copy { - $0.frame = rect - $0.backgroundColor = color - $0.layerBorderColor = color - $0.layerBorderWidth = float - $0.layerCornerRadius = float - $0.alpha = float - $0.isHidden = boolean - $0.intrinsicContentSize = rect.size - $0.overrides = overrides - } - XCTAssertEqual(attributes.frame, rect) - XCTAssertEqual(attributes.backgroundColor, color) - XCTAssertEqual(attributes.layerBorderColor, color) - XCTAssertEqual(attributes.layerBorderWidth, float) - XCTAssertEqual(attributes.layerCornerRadius, float) - XCTAssertEqual(attributes.alpha, float) - XCTAssertEqual(attributes.isHidden, boolean) - XCTAssertEqual(attributes.intrinsicContentSize, rect.size) - XCTAssertEqual(attributes.overrides, overrides) - } - // MARK: Privacy Overrides func testItDefaultsToNilWhenNoOverrideIsSet() { @@ -210,7 +185,7 @@ class ViewAttributesTests: XCTestCase { let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) // When - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 1) @@ -228,7 +203,7 @@ class ViewAttributesTests: XCTestCase { let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) // When - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 1, "Child view overrides parent's hidden state, so it should be recorded.") @@ -246,7 +221,7 @@ class ViewAttributesTests: XCTestCase { parentView.dd.sessionReplayPrivacyOverrides.touchPrivacy = parentOverrides.touchPrivacy let recorder = ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()) - let nodes = recorder.record(parentView, in: .mockRandom()) + let nodes = recorder.record(parentView, in: .mockWith(coordinateSpace: parentView)) // Then XCTAssertEqual(nodes.count, 2) @@ -296,8 +271,13 @@ class NodeSemanticsTests: XCTestCase { } extension ViewAttributesTests { - func createViewAttributes(with view: UIView) -> ViewAttributes { - return ViewAttributes(frameInRootView: view.frame, view: view, overrides: .mockAny()) + func createViewAttributes(with view: UIView, clip: CGRect? = nil) -> ViewAttributes { + ViewAttributes( + view: view, + frame: view.frame, + clip: clip ?? view.frame, + overrides: .mockAny() + ) } } #endif From ac61e4f1292d6f6affbb9b1be1c06776bb8630a9 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Fri, 18 Oct 2024 15:18:38 +0200 Subject: [PATCH 14/38] RUM-6118 Add clip support to snapshot tests --- .../Resources/Storyboards/Basic.storyboard | 47 +++- .../SRSnapshotTests.xcodeproj/project.pbxproj | 2 +- .../Utils/ImageRendering.swift | 217 +++++++++++------- ...pes()-maskSensitiveInputs-privacy.png.json | 2 +- .../testBasicTexts()-maskAll-privacy.png.json | 2 +- ...asicTexts()-maskAllInputs-privacy.png.json | 2 +- ...xts()-maskSensitiveInputs-privacy.png.json | 2 +- 7 files changed, 173 insertions(+), 101 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard index 464f532a0e..e126728b83 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -18,12 +17,24 @@ - + + + + + + + + + + + + + @@ -46,6 +57,16 @@ + + + + + + + + + + @@ -140,7 +161,7 @@ - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laborissunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laborissunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laborissunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. @@ -232,22 +253,28 @@ - + + + + - + - + - + + + + - + diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj index 3a9d731a59..be77c68c1c 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests.xcodeproj/project.pbxproj @@ -565,7 +565,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ncreated/Framer"; requirement = { - branch = "ncreated-patch-1"; + branch = main; kind = branch; }; }; diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift index 4d1d20d1a6..d0f36d8a45 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/ImageRendering.swift @@ -75,11 +75,13 @@ private extension SRWireframe { private extension SRShapeWireframe { func toFrame() -> BlueprintFrame { BlueprintFrame( - x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height), - style: frameStyle(border: border, style: shapeStyle), + x: x, + y: y, + width: width, + height: height, + border: border, + style: shapeStyle, + clip: clip, content: nil ) } @@ -88,12 +90,14 @@ private extension SRShapeWireframe { private extension SRTextWireframe { func toFrame() -> BlueprintFrame { BlueprintFrame( - x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height), - style: frameStyle(border: border, style: shapeStyle), - content: frameContent(text: text, textStyle: textStyle, textPosition: textPosition) + x: x, + y: y, + width: width, + height: height, + border: border, + style: shapeStyle, + clip: clip, + content: .init(text: text, textStyle: textStyle, textPosition: textPosition) ) } } @@ -101,12 +105,14 @@ private extension SRTextWireframe { private extension SRImageWireframe { func toFrame(imageData: Data?) -> BlueprintFrame { BlueprintFrame( - x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height), - style: frameStyle(border: border, style: shapeStyle), - content: frameContent(imageData: imageData) + x: x, + y: y, + width: width, + height: height, + border: border, + style: shapeStyle, + clip: clip, + content: .init(imageData: imageData) ) } } @@ -114,12 +120,14 @@ private extension SRImageWireframe { private extension SRWebviewWireframe { func toFrame() -> BlueprintFrame { BlueprintFrame( - x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height), - style: frameStyle(border: border, style: shapeStyle), - content: frameContent( + x: x, + y: y, + width: width, + height: height, + border: border, + style: shapeStyle, + clip: clip, + content: .init( text: "WKWebView", textStyle: nil, textPosition: SRTextPosition( @@ -134,19 +142,18 @@ private extension SRWebviewWireframe { private extension SRPlaceholderWireframe { func toFrame() -> BlueprintFrame { BlueprintFrame( - x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height), - style: frameStyle( - border: .init(color: "#000000FF", width: 4), - style: .init( - backgroundColor: "#A9A9A9FF", - cornerRadius: 0, - opacity: 1 - ) + x: x, + y: y, + width: width, + height: height, + border: .init(color: "#000000FF", width: 4), + style: .init( + backgroundColor: "#A9A9A9FF", + cornerRadius: 0, + opacity: 1 ), - content: frameContent( + clip: clip, + content: .init( text: label ?? "Placeholder", textStyle: .init(color: "#000000FF", family: "-apple-system", size: 24), textPosition: .init( @@ -158,71 +165,109 @@ private extension SRPlaceholderWireframe { } } +private extension BlueprintFrame { + init( + x: Int64, + y: Int64, + width: Int64, + height: Int64, + border: SRShapeBorder?, + style: SRShapeStyle?, + clip: SRContentClip?, + content: BlueprintFrame.Content? + ) { + var frame = BlueprintFrame( + x: CGFloat(x), + y: CGFloat(y), + width: CGFloat(width), + height: CGFloat(height), + content: content + ) + + var fs = BlueprintFrame.Style( + lineWidth: 0, + lineColor: .clear, + fillColor: .clear, + cornerRadius: 0, + opacity: 1 + ) -private func frameStyle(border: SRShapeBorder?, style: SRShapeStyle?) -> BlueprintFrame.Style { - var fs = BlueprintFrame.Style( - lineWidth: 0, - lineColor: .clear, - fillColor: .clear, - cornerRadius: 0, - opacity: 1 - ) + if let border = border { + fs.lineWidth = CGFloat(border.width) + fs.lineColor = UIColor(hexString: border.color) + } - if let border = border { - fs.lineWidth = CGFloat(border.width) - fs.lineColor = UIColor(hexString: border.color) - } + if let style = style { + fs.fillColor = style.backgroundColor.flatMap { UIColor(hexString: $0) } ?? fs.fillColor + fs.cornerRadius = style.cornerRadius.flatMap { CGFloat($0) } ?? fs.cornerRadius + fs.opacity = style.opacity.flatMap { CGFloat($0) } ?? fs.opacity + } + + if let clip = clip { + fs.clip = CGRect( + x: frame.x, + y: frame.y, + width: frame.width, + height: frame.height + ).inset( + by: UIEdgeInsets( + top: clip.top.map { CGFloat($0) } ?? 0, + left: clip.left.map { CGFloat($0) } ?? 0, + bottom: clip.bottom.map { CGFloat($0) } ?? 0, + right: clip.right.map { CGFloat($0) } ?? 0 + ) + ) + } - if let style = style { - fs.fillColor = style.backgroundColor.flatMap { UIColor(hexString: $0) } ?? fs.fillColor - fs.cornerRadius = style.cornerRadius.flatMap { CGFloat($0) } ?? fs.cornerRadius - fs.opacity = style.opacity.flatMap { CGFloat($0) } ?? fs.opacity + frame.style = fs + self = frame } - return fs } -private func frameContent(text: String, textStyle: SRTextStyle?, textPosition: SRTextPosition?) -> BlueprintFrame.Content { - var horizontalAlignment: BlueprintFrame.Content.Alignment = .leading - var verticalAlignment: BlueprintFrame.Content.Alignment = .leading +extension BlueprintFrame.Content { + init(text: String, textStyle: SRTextStyle?, textPosition: SRTextPosition?) { + var horizontalAlignment: BlueprintFrame.Content.Alignment = .leading + var verticalAlignment: BlueprintFrame.Content.Alignment = .leading - if let textPosition = textPosition { - switch textPosition.alignment?.horizontal { - case .left?: horizontalAlignment = .leading - case .center?: horizontalAlignment = .center - case .right?: horizontalAlignment = .trailing - default: break + if let textPosition = textPosition { + switch textPosition.alignment?.horizontal { + case .left?: horizontalAlignment = .leading + case .center?: horizontalAlignment = .center + case .right?: horizontalAlignment = .trailing + default: break + } + switch textPosition.alignment?.vertical { + case .top?: verticalAlignment = .leading + case .center?: verticalAlignment = .center + case .bottom?: verticalAlignment = .trailing + default: break + } } - switch textPosition.alignment?.vertical { - case .top?: verticalAlignment = .leading - case .center?: verticalAlignment = .center - case .bottom?: verticalAlignment = .trailing - default: break + + let contentType: BlueprintFrame.Content.ContentType + if let textStyle = textStyle { + contentType = .text( + text: text, + color: UIColor(hexString: textStyle.color), + font: .systemFont(ofSize: CGFloat(textStyle.size)) + ) + } else { + contentType = .text(text: text, color: .clear, font: .systemFont(ofSize: 8)) } - } - let contentType: BlueprintFrame.Content.ContentType - if let textStyle = textStyle { - contentType = .text( - text: text, - color: UIColor(hexString: textStyle.color), - font: .systemFont(ofSize: CGFloat(textStyle.size)) + self.init( + contentType: contentType, + horizontalAlignment: horizontalAlignment, + verticalAlignment: verticalAlignment ) - } else { - contentType = .text(text: text, color: .clear, font: .systemFont(ofSize: 8)) } - return .init( - contentType: contentType, - horizontalAlignment: horizontalAlignment, - verticalAlignment: verticalAlignment - ) -} - -private func frameContent(imageData: Data?) -> BlueprintFrame.Content { - let image = UIImage(data: imageData ?? Data(), scale: UIScreen.main.scale) ?? UIImage() - let contentType: BlueprintFrame.Content.ContentType = .image(image: image) - return .init(contentType: contentType) + init(imageData: Data?) { + let image = UIImage(data: imageData ?? Data(), scale: UIScreen.main.scale) ?? UIImage() + let contentType: BlueprintFrame.Content.ContentType = .image(image: image) + self.init(contentType: contentType) + } } private extension UIColor { diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json index b35f737cef..ba26a5e416 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"a319355bc86680b04a87e57b66a7e3988ddd61e8"} \ No newline at end of file +{"hash":"86b2de22fa6ae7ce08453e49a19d4d608e845c45"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAll-privacy.png.json index 53180e8dd1..71dac4ccb1 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"b230c301c41d0ac009396b7daac7fe99bf95f9ee"} \ No newline at end of file +{"hash":"37f85d30de6a88ac5bae9e67343c595d27eb2019"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAllInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAllInputs-privacy.png.json index 46d9ed1575..07fb6ee408 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAllInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskAllInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"5d2a6e27e44e5d5a82bd64a8f64ca11fe2f9d778"} \ No newline at end of file +{"hash":"96a2c248a5a5233938b8bdb99bf49c067350b1d6"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskSensitiveInputs-privacy.png.json index a66be52d33..9eb65715da 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicTexts()-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"2178a198f86cfe64d4b7f9be90918a489a4b3d56"} \ No newline at end of file +{"hash":"2ed368c1eb7d16a67c62f0da52c8904af54a039c"} \ No newline at end of file From 1c8db89dccb24bcb3c442781e14874bf67491365 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Fri, 18 Oct 2024 16:26:22 +0200 Subject: [PATCH 15/38] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f327debeb..ccbd360cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - [FEATURE] Add Privacy Overrides in Session Replay. See [#2088][] - [IMPROVEMENT] Add ObjC API for the internal logging/telemetry. See [#2073][] +- [IMPROVEMENT] Support `clipsToBounds` in Session Replay. See [#2083][] # 2.18.0 / 25-09-2024 - [IMPROVEMENT] Add overwrite required (breaking) param to addViewLoadingTime & usage telemetry. See [#2040][] @@ -781,6 +782,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#2050]: https://github.com/DataDog/dd-sdk-ios/pull/2050 [#2073]: https://github.com/DataDog/dd-sdk-ios/pull/2073 [#2088]: https://github.com/DataDog/dd-sdk-ios/pull/2088 +[#2083]: https://github.com/DataDog/dd-sdk-ios/pull/2083 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin [@hengyu]: https://github.com/Hengyu From bf99a547d4f023a2c6d442a06e2600d6b7713ddd Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Fri, 18 Oct 2024 18:06:00 +0200 Subject: [PATCH 16/38] Update snapshot fixtures --- .../testBasicShapes()-maskSensitiveInputs-privacy.png.json | 2 +- .../pointers/testImages_MaskNone()-maskAll-privacy.png.json | 2 +- .../testImages_MaskNone()-maskSensitiveInputs-privacy.png.json | 2 +- ...ednavigationbarblack+nontranslucent-maskAll-privacy.png.json | 2 +- ...barblack+nontranslucent-maskSensitiveInputs-privacy.png.json | 2 +- ...eddednavigationbarblack+translucent-maskAll-privacy.png.json | 2 +- ...ionbarblack+translucent-maskSensitiveInputs-privacy.png.json | 2 +- ...ault+nontranslucent+backgroundcolor-maskAll-privacy.png.json | 2 +- ...slucent+backgroundcolor-maskSensitiveInputs-privacy.png.json | 2 +- ...onbardefault+nontranslucent+bartint-maskAll-privacy.png.json | 2 +- ...+nontranslucent+bartint-maskSensitiveInputs-privacy.png.json | 2 +- ...navigationbardefault+nontranslucent-maskAll-privacy.png.json | 2 +- ...rdefault+nontranslucent-maskSensitiveInputs-privacy.png.json | 2 +- ...default+translucent+backgroundcolor-maskAll-privacy.png.json | 2 +- ...slucent+backgroundcolor-maskSensitiveInputs-privacy.png.json | 2 +- ...ationbardefault+translucent+bartint-maskAll-privacy.png.json | 2 +- ...ult+translucent+bartint-maskSensitiveInputs-privacy.png.json | 2 +- ...dednavigationbardefault+translucent-maskAll-privacy.png.json | 2 +- ...nbardefault+translucent-maskSensitiveInputs-privacy.png.json | 2 +- .../testTabBars()-embeddedtabbar-maskAll-privacy.png.json | 2 +- ...abBars()-embeddedtabbar-maskSensitiveInputs-privacy.png.json | 2 +- ...)-embeddedtabbarunselectedtintcolor-maskAll-privacy.png.json | 2 +- ...bbarunselectedtintcolor-maskSensitiveInputs-privacy.png.json | 2 +- .../Sources/Processor/Builders/WireframesBuilder.swift | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json index ba26a5e416..7728d7b3d7 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testBasicShapes()-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"86b2de22fa6ae7ce08453e49a19d4d608e845c45"} \ No newline at end of file +{"hash":"520473194d20efe7adec811280dbf35175a1add7"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskAll-privacy.png.json index f8ea11dec3..ca0bbc2da4 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"44988fd9bd4a7cb6fc46c5e0c6b541a1465c3a99"} \ No newline at end of file +{"hash":"4c0048882009119a166163caebc53829dad94aa2"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskSensitiveInputs-privacy.png.json index b2f2c7b55b..d7ae0a4063 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testImages_MaskNone()-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"e4d535631325561dbf538f817b3b5042d5e123c0"} \ No newline at end of file +{"hash":"a74702fdab929afaacda0af91b88f010851df45f"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskAll-privacy.png.json index fd69c2e90a..4ebc942ec7 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"a0e8532416b24531b708fc6d63336fa497fca50a"} \ No newline at end of file +{"hash":"02480f0661fe43ad696a876a46211c20c440b7d5"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskSensitiveInputs-privacy.png.json index 5cacbaf278..663cb0b260 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+nontranslucent-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"70dd71eeb205248f4e485461f6256e03a2278966"} \ No newline at end of file +{"hash":"1a27cde2deffbb53dc87b4c46bba2dd23c975d1b"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskAll-privacy.png.json index 8015bf4853..0c8a77f1ea 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"584de5404d40b3c047349ae54e706ec9671c82fe"} \ No newline at end of file +{"hash":"306708a6ca98429b9ca40086cda69076220624b8"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskSensitiveInputs-privacy.png.json index fb6da026fa..01465ad7ed 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbarblack+translucent-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"bf37b098592b844040fe9e55246cff9c68b36086"} \ No newline at end of file +{"hash":"3d0be39d01b9d99cca939a9dd3c983542b2154df"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskAll-privacy.png.json index 24314afe67..16fb9f1e40 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"dc4e399a0f2f240a123a91d608f5f37f87dadccf"} \ No newline at end of file +{"hash":"2864f48a439157c1e04e425b5092f1cebf615f38"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskSensitiveInputs-privacy.png.json index c7a582afdf..99961b4169 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+backgroundcolor-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"65116e84bf3e2cd2c8424d8fe844537f19aced70"} \ No newline at end of file +{"hash":"bdb6768258f21a44c6d63a9695401be8fd1a8cf4"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskAll-privacy.png.json index a172304e23..a876c5b7fc 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"4d50f9a00b6f17e2f2993cec6f1242abc69297bf"} \ No newline at end of file +{"hash":"4d0b11013b656f4dd5204711e45eebdc9fa52076"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskSensitiveInputs-privacy.png.json index fa9b7a47e7..a33fa78231 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent+bartint-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"e3f7d09e522c85bb027d175781e9031773f8eb17"} \ No newline at end of file +{"hash":"715454a57bac504f256ab73e2ac241959d2f36c3"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskAll-privacy.png.json index c6a2fa7db3..3e3ca673f9 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"51fac961ad0b2ca372aaa86d776d52b226ce8ae9"} \ No newline at end of file +{"hash":"fb29c5150dba8e9f4867c3f43bfcaabe9e8c24a2"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskSensitiveInputs-privacy.png.json index 272d323ebd..567061208f 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+nontranslucent-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"e4fe8b27c3be2c4fb3fc525515564e7f92766d7a"} \ No newline at end of file +{"hash":"032eb0ea99b24624b2b55bc69765664ad23e2f0f"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskAll-privacy.png.json index aa9a10f407..66a88326ae 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"6ceaca571214d71bcd89a756b511b16afe8fe7b1"} \ No newline at end of file +{"hash":"14949ededf750b78161fb466e1bc25e7fcdb5594"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskSensitiveInputs-privacy.png.json index bff7d5e960..ae39862c0a 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+backgroundcolor-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"9251f5880fc20349d6eed42850d5f1fe28d144a5"} \ No newline at end of file +{"hash":"1436d6308e2fc58d53da5ce7d7d56ecdcae80daa"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskAll-privacy.png.json index 60d197b3a6..b1ac3e3d77 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"2f7af9a5e78240d7b9788f39de76bb85545b9cca"} \ No newline at end of file +{"hash":"80b64b6019c1f88757492e95adb89e12e8631dd1"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskSensitiveInputs-privacy.png.json index b231ad4495..93424412a8 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent+bartint-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"2028108461c50844f72464fba048ca9572600976"} \ No newline at end of file +{"hash":"5f38e9f3377d5da51a3e797177cceda0d4617144"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskAll-privacy.png.json index 8669483681..08186b996c 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"144baafb1b16cb5debd06b5a6a48038c76471318"} \ No newline at end of file +{"hash":"a3d643911e5f4911737fb4f3b06a3a359fed88ee"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskSensitiveInputs-privacy.png.json index 463af43636..fefefbebbb 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testNavigationBars()-embeddednavigationbardefault+translucent-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"aa8f881b3c0fc6622e7dac04bd0b1910288960cf"} \ No newline at end of file +{"hash":"4de07be1ae6566d1b3d4041869e3a0bf3adba159"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskAll-privacy.png.json index 25b9228b0f..da511c353b 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"eca45148dd435edb6c3f40eb6b610c120f274199"} \ No newline at end of file +{"hash":"3163f4935cf9527eed8c81174cb0e4e005d91dff"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskSensitiveInputs-privacy.png.json index de2ea57823..b50877822f 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbar-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"b8e58a4dac63afb1bce3f7c9411273e5e3efbee2"} \ No newline at end of file +{"hash":"8cb0c342ee007ace1ae30b11087c19a78a225667"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskAll-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskAll-privacy.png.json index cef0139a67..a1523b033a 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskAll-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskAll-privacy.png.json @@ -1 +1 @@ -{"hash":"caff10297d82490f314b3be492f025d9702e642e"} \ No newline at end of file +{"hash":"86b5ccff063182050f4e23a3c515be5e865359a5"} \ No newline at end of file diff --git a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskSensitiveInputs-privacy.png.json b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskSensitiveInputs-privacy.png.json index ed9b0c81b3..f4d82d0356 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskSensitiveInputs-privacy.png.json +++ b/DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testTabBars()-embeddedtabbarunselectedtintcolor-maskSensitiveInputs-privacy.png.json @@ -1 +1 @@ -{"hash":"6cc9c4b6ed985d68ffe07c9e1524b7e36177b5ef"} \ No newline at end of file +{"hash":"68f5df7445d22cd7017f71b7381815469bb5fc06"} \ No newline at end of file diff --git a/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift index d808722453..bddf19bfa6 100644 --- a/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift @@ -349,7 +349,7 @@ extension SRContentClip { let right = frame.maxX - intersection.maxX // more reliable than intersection == frame - if bottom.isZero, bottom.isZero, bottom.isZero, bottom.isZero { + if top.isZero, left.isZero, bottom.isZero, right.isZero { return nil } From 029c329c22af32a925f728424b94535bc29042e0 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Tue, 29 Oct 2024 17:26:43 +0100 Subject: [PATCH 17/38] Improve comments Signed-off-by: Maxime Epain --- .../Builders/WireframesBuilder.swift | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift index bddf19bfa6..3c0f5497f4 100644 --- a/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift +++ b/DatadogSessionReplay/Sources/Processor/Builders/WireframesBuilder.swift @@ -312,27 +312,22 @@ extension SRContentClip { ) } - /// Creates Content Clip by intersecting the frame with the clipping rectangle. + /// Creates a Content Clip by intersecting the view frame with the clipping rectangle. /// - /// If the clip rectangle does not intersect with the frame, Content Clip will be initialised with: + /// The clipping rectangle usually represents the bounds of the parent view when `clipsToBounds` is enabled. + /// It determines which portion of the view should remain visible after clipping. /// - /// SRContentClip( - /// bottom: nil, - /// left: frame.width, - /// right: nil, - /// top: frame.height - /// ) - /// - /// This will result in a wireframe with no drawing area. Recorders should, in practice, prevent + /// If the intersection is empty, the view is completely clipped, and the resulting clip dimensions reflect the view’s size, + /// indicating no visible content. This will result in a wireframe with no drawing area, recorders should, in practice, prevent /// this use case. /// /// - Parameters: /// - frame: The view frame. - /// - clip: The clipping rectangle. + /// - clip: The clipping rectangle representing the visible area. init?(_ frame: CGRect, intersecting clip: CGRect) { let intersection = frame.intersection(clip) - if intersection.isEmpty { + guard !intersection.isEmpty else { self.init( bottom: nil, left: Int64(withNoOverflow: frame.width), From dc036faf68d09af6401ccc20a0548ef2d41386f7 Mon Sep 17 00:00:00 2001 From: Maxime Epain Date: Thu, 24 Oct 2024 16:27:27 +0200 Subject: [PATCH 18/38] Remove force unwrapping --- ...tworkInstrumentationIntegrationTests.swift | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift b/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift index 49dca3dcfd..649534d9c3 100644 --- a/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift +++ b/Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift @@ -107,9 +107,7 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { applicationID: .mockAny(), urlSessionTracking: .init( resourceAttributesProvider: { req, resp, data, err in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - providerDataCount = data!.count + providerDataCount = data?.count ?? 0 providerExpectation.fulfill() return [:] }) @@ -149,17 +147,14 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { ) let providerExpectation = expectation(description: "provider called") - var providerDataCount = 0 - var providerData: Data? + var providerInfo: (resp: URLResponse?, data: Data?, err: Error?)? + RUM.enable( with: .init( applicationID: .mockAny(), urlSessionTracking: .init( - resourceAttributesProvider: { req, resp, data, err in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - providerDataCount = data!.count - data.map { providerData = $0 } + resourceAttributesProvider: { _, resp, data, err in + providerInfo = (resp, data, err) providerExpectation.fulfill() return [:] }) @@ -182,20 +177,18 @@ class NetworkInstrumentationIntegrationTests: XCTestCase { let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!) let taskExpectation = self.expectation(description: "task completed") - var taskDataCount = 0 - var taskData: Data? - let task = session.dataTask(with: request) { data, _, _ in - XCTAssertNotNil(data) - XCTAssertTrue(data!.count > 0) - taskDataCount = data!.count - data.map { taskData = $0 } + var taskInfo: (resp: URLResponse?, data: Data?, err: Error?)? + + let task = session.dataTask(with: request) { resp, data, err in + taskInfo = (data, resp, err) taskExpectation.fulfill() } task.resume() wait(for: [providerExpectation, taskExpectation], timeout: 10) - XCTAssertEqual(providerDataCount, taskDataCount) - XCTAssertEqual(providerData, taskData) + XCTAssertEqual(providerInfo?.resp, taskInfo?.resp) + XCTAssertEqual(providerInfo?.data, taskInfo?.data) + XCTAssertEqual(providerInfo?.err as? NSError, taskInfo?.err as? NSError) } class InstrumentedSessionDelegate: NSObject, URLSessionDataDelegate { From 81150a66f4f9116cc34e210c8ec1f3ad735d83a9 Mon Sep 17 00:00:00 2001 From: Marie Denis Date: Wed, 30 Oct 2024 16:30:12 +0100 Subject: [PATCH 19/38] RUM-6425 [SR] Include Privacy Overrides in Snapshot Tests --- .../Resources/Storyboards/Basic.storyboard | 11 ++- .../Resources/Storyboards/Images.storyboard | 8 +- .../SRSnapshotTests.xcodeproj/project.pbxproj | 4 + .../SRSnapshotTests/SRSnapshotTests.swift | 99 +++++++++++++++++++ .../Utils/PrivacyOverrides.swift | 85 ++++++++++++++++ .../Utils/SnapshotTestCase.swift | 12 ++- ...sking-maskSensitiveInputs-privacy.png.json | 1 + ...sking-maskSensitiveInputs-privacy.png.json | 1 + ...sking-maskSensitiveInputs-privacy.png.json | 1 + ...tView-maskSensitiveInputs-privacy.png.json | 1 + ...tView-maskSensitiveInputs-privacy.png.json | 1 + ...tView-maskSensitiveInputs-privacy.png.json | 1 + ...sking-maskSensitiveInputs-privacy.png.json | 1 + ...errides_unmasking-maskAll-privacy.png.json | 1 + 14 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/Utils/PrivacyOverrides.swift create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testMaskingPrivacyOverrides()-hideOverride_masking-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testMaskingPrivacyOverrides()-imageOverrides_masking-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testMaskingPrivacyOverrides()-textOverrides_masking-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testMaskingPrivacyOverridesOnParentView()-hideOverride_masking_parentView-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testMaskingPrivacyOverridesOnParentView()-imageOverride_masking_parentView-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testMaskingPrivacyOverridesOnParentView()-textOverride_masking_parentView-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testUnmaskingPrivacyOverrides()-imageOverrides_unmasking-maskSensitiveInputs-privacy.png.json create mode 100644 DatadogSessionReplay/SRSnapshotTests/SRSnapshotTests/_snapshots_/pointers/testUnmaskingPrivacyOverrides()-textOverrides_unmasking-maskAll-privacy.png.json diff --git a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard index e126728b83..540c639cba 100644 --- a/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard +++ b/DatadogSessionReplay/SRSnapshotTests/SRFixtures/Sources/SRFixtures/Resources/Storyboards/Basic.storyboard @@ -2,6 +2,7 @@ + @@ -17,10 +18,10 @@ - + - + @@ -98,7 +99,7 @@ - + -