diff --git a/Sources/Datadog/Core/Feature.swift b/Sources/Datadog/Core/Feature.swift index 577158aae0..6ea0194a3d 100644 --- a/Sources/Datadog/Core/Feature.swift +++ b/Sources/Datadog/Core/Feature.swift @@ -23,12 +23,15 @@ internal struct FeaturesCommonDependencies { let performance: PerformancePreset let httpClient: HTTPClient let mobileDevice: MobileDevice + /// Time of SDK initialization, measured in device date. + let sdkInitDate: Date let dateProvider: DateProvider let dateCorrector: DateCorrectorType let userInfoProvider: UserInfoProvider let networkConnectionInfoProvider: NetworkConnectionInfoProviderType let carrierInfoProvider: CarrierInfoProviderType let launchTimeProvider: LaunchTimeProviderType + let appStateListener: AppStateListening } internal struct FeatureStorage { diff --git a/Sources/Datadog/Core/System/AppStateListener.swift b/Sources/Datadog/Core/System/AppStateListener.swift index c1081b4610..850a53f31d 100644 --- a/Sources/Datadog/Core/System/AppStateListener.swift +++ b/Sources/Datadog/Core/System/AppStateListener.swift @@ -11,17 +11,23 @@ import class UIKit.UIApplication internal struct AppStateHistory: Equatable { /// Snapshot of the app state at `date` struct Snapshot: Equatable { + /// If the app is running in the foreground and currently receiving events. let isActive: Bool + /// Date of recording this snapshot. let date: Date } - var initialState: Snapshot - var changes = [Snapshot]() - var finalDate: Date - var finalState: Snapshot { + fileprivate(set) var initialState: Snapshot + fileprivate(set) var changes = [Snapshot]() + + /// Date of last the update to `AppStateHistory`. + fileprivate(set) var recentDate: Date + + /// The most recent app state `Snapshot`. + var currentState: Snapshot { return Snapshot( isActive: (changes.last ?? initialState).isActive, - date: finalDate + date: recentDate ) } @@ -37,7 +43,7 @@ internal struct AppStateHistory: Equatable { date: range.lowerBound ) // move final state to upperBound - taken.finalDate = range.upperBound + taken.recentDate = range.upperBound // filter changes outside of the range taken.changes = taken.changes.filter { range.contains($0.date) } return taken @@ -46,7 +52,7 @@ internal struct AppStateHistory: Equatable { var foregroundDuration: TimeInterval { var duration: TimeInterval = 0.0 var lastActiveStartDate: Date? - let allEvents = [initialState] + changes + [finalState] + let allEvents = [initialState] + changes + [currentState] for event in allEvents { if let startDate = lastActiveStartDate { duration += event.date.timeIntervalSince(startDate) @@ -61,16 +67,16 @@ internal struct AppStateHistory: Equatable { } var didRunInBackground: Bool { - return !initialState.isActive || !finalState.isActive + return !initialState.isActive || !currentState.isActive } private func isActive(at date: Date) -> Bool { if date <= initialState.date { // we assume there was no change before initial state return initialState.isActive - } else if finalState.date <= date { + } else if currentState.date <= date { // and no change after final state - return finalState.isActive + return currentState.isActive } var active = initialState for change in changes { @@ -83,6 +89,7 @@ internal struct AppStateHistory: Equatable { } } +/// Provides history of app foreground / background states. internal protocol AppStateListening: AnyObject { var history: AppStateHistory { get } } @@ -95,7 +102,7 @@ internal class AppStateListener: AppStateListening { var history: AppStateHistory { var current = publisher.currentValue - current.finalDate = dateProvider.currentDate() + current.recentDate = dateProvider.currentDate() return current } @@ -115,7 +122,7 @@ internal class AppStateListener: AppStateListening { self.publisher = ValuePublisher( initialValue: AppStateHistory( initialState: currentState, - finalDate: currentState.date + recentDate: currentState.date ) ) diff --git a/Sources/Datadog/Datadog.swift b/Sources/Datadog/Datadog.swift index 0fe6d96e6d..0c32a83d37 100644 --- a/Sources/Datadog/Datadog.swift +++ b/Sources/Datadog/Datadog.swift @@ -212,12 +212,14 @@ public class Datadog { performance: configuration.common.performance, httpClient: HTTPClient(proxyConfiguration: configuration.common.proxyConfiguration), mobileDevice: MobileDevice(), + sdkInitDate: dateProvider.currentDate(), dateProvider: dateProvider, dateCorrector: dateCorrector, userInfoProvider: userInfoProvider, networkConnectionInfoProvider: networkConnectionInfoProvider, carrierInfoProvider: carrierInfoProvider, - launchTimeProvider: launchTimeProvider + launchTimeProvider: launchTimeProvider, + appStateListener: AppStateListener(dateProvider: dateProvider) ) if let internalMonitoringConfiguration = configuration.internalMonitoring { @@ -273,8 +275,7 @@ public class Datadog { if let urlSessionAutoInstrumentationConfiguration = configuration.urlSessionAutoInstrumentation { urlSessionAutoInstrumentation = URLSessionAutoInstrumentation( configuration: urlSessionAutoInstrumentationConfiguration, - dateProvider: dateProvider, - appStateListener: AppStateListener(dateProvider: dateProvider) + commonDependencies: commonDependencies ) } diff --git a/Sources/Datadog/RUM/RUMFeature.swift b/Sources/Datadog/RUM/RUMFeature.swift index ef21445f43..e802d4562a 100644 --- a/Sources/Datadog/RUM/RUMFeature.swift +++ b/Sources/Datadog/RUM/RUMFeature.swift @@ -30,8 +30,10 @@ internal final class RUMFeature { // MARK: - Dependencies + let sdkInitDate: Date let dateProvider: DateProvider let dateCorrector: DateCorrectorType + let appStateListener: AppStateListening let userInfoProvider: UserInfoProvider let networkConnectionInfoProvider: NetworkConnectionInfoProviderType let carrierInfoProvider: CarrierInfoProviderType @@ -167,8 +169,10 @@ internal final class RUMFeature { self.configuration = configuration // Bundle dependencies + self.sdkInitDate = commonDependencies.sdkInitDate self.dateProvider = commonDependencies.dateProvider self.dateCorrector = commonDependencies.dateCorrector + self.appStateListener = commonDependencies.appStateListener self.userInfoProvider = commonDependencies.userInfoProvider self.networkConnectionInfoProvider = commonDependencies.networkConnectionInfoProvider self.carrierInfoProvider = commonDependencies.carrierInfoProvider diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 5cc0610a86..2fa181b983 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -10,6 +10,7 @@ internal typealias RUMSessionListener = (String, Bool) -> Void /// Injection container for common dependencies used by all `RUMScopes`. internal struct RUMScopeDependencies { + let appStateListener: AppStateListening let userInfoProvider: RUMUserInfoProvider let launchTimeProvider: LaunchTimeProviderType let connectivityInfoProvider: RUMConnectivityInfoProvider @@ -36,6 +37,9 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { /// RUM Sessions sampling rate. internal let samplingRate: Float + /// The start time of the application, measured in device date. It equals the time of SDK init. + private let applicationStartTime: Date + /// Automatically detect background events internal let backgroundEventTrackingEnabled: Bool @@ -47,10 +51,12 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { rumApplicationID: String, dependencies: RUMScopeDependencies, samplingRate: Float, + applicationStartTime: Date, backgroundEventTrackingEnabled: Bool ) { self.dependencies = dependencies self.samplingRate = samplingRate + self.applicationStartTime = applicationStartTime self.backgroundEventTrackingEnabled = backgroundEventTrackingEnabled self.context = RUMContext( rumApplicationID: rumApplicationID, @@ -70,7 +76,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { func process(command: RUMCommand) -> Bool { if sessionScope == nil { - startInitialSession(on: command) + startInitialSession() } if let currentSession = sessionScope { @@ -93,13 +99,13 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { _ = refreshedSession.process(command: command) } - private func startInitialSession(on command: RUMCommand) { + private func startInitialSession() { let initialSession = RUMSessionScope( isInitialSession: true, parent: self, dependencies: dependencies, samplingRate: samplingRate, - startTime: command.time, + startTime: applicationStartTime, backgroundEventTrackingEnabled: backgroundEventTrackingEnabled ) sessionScope = initialSession diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 299a44c742..2e6e29d2d1 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -49,7 +49,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { let isInitialSession: Bool /// RUM Session sampling rate. private let samplingRate: Float - /// The start time of this Session. + /// The start time of this Session, measured in device date. In initial session this is the time of SDK init. private let sessionStartTime: Date /// Time of the last RUM interaction noticed by this Session. private var lastInteractionTime: Date @@ -129,9 +129,14 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // Consider starting an active view, "ApplicationLaunch" view or "Background" view if let startViewCommand = command as? RUMStartViewCommand { startView(on: startViewCommand) - } else if isInitialSession && !hasTrackedAnyView && command.canStartApplicationLaunchView { - startApplicationLaunchView(on: command) - } else if backgroundEventTrackingEnabled && !hasActiveView && command.canStartBackgroundView { + } else if isInitialSession && !hasTrackedAnyView { // if initial session with no views history + let appInForeground = dependencies.appStateListener.history.currentState.isActive + if appInForeground && command.canStartApplicationLaunchView { // when app is in foreground, start "ApplicationLaunch" view + startApplicationLaunchView(on: command) + } else if backgroundEventTrackingEnabled && command.canStartBackgroundView { // when app is in background and BET is enabled, start "Background" view + startBackgroundView(on: command) + } + } else if backgroundEventTrackingEnabled && !hasActiveView && command.canStartBackgroundView { // if existing session with views history and BET is enabled startBackgroundView(on: command) } @@ -186,7 +191,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { name: Constants.applicationLaunchViewName, attributes: command.attributes, customTimings: [:], - startTime: command.time + startTime: sessionStartTime ) ) } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index b1f1b0dfec..155c9dfc5f 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -171,9 +171,14 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { } case let command as RUMAddUserActionCommand where isActiveView: if userActionScope == nil { - addDiscreteUserAction(on: command) + if command.actionType == .custom { + // send it instantly without waiting for child events (e.g. resource associated to this action) + sendDiscreteCustomUserAction(on: command) + } else { + addDiscreteUserAction(on: command) + } } else if command.actionType == .custom { - // still let it go, just instantly without any dependencies + // still let it go, just instantly without waiting for child events (e.g. resource associated to this action) sendDiscreteCustomUserAction(on: command) } else { reportActionDropped(type: command.actionType, name: command.name) diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index 13510f9cae..f4824f8198 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -178,6 +178,7 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { applicationScope: RUMApplicationScope( rumApplicationID: rumFeature.configuration.applicationID, dependencies: RUMScopeDependencies( + appStateListener: rumFeature.appStateListener, userInfoProvider: RUMUserInfoProvider(userInfoProvider: rumFeature.userInfoProvider), launchTimeProvider: rumFeature.launchTimeProvider, connectivityInfoProvider: RUMConnectivityInfoProvider( @@ -198,6 +199,7 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { onSessionStart: rumFeature.onSessionStart ), samplingRate: rumFeature.configuration.sessionSamplingRate, + applicationStartTime: rumFeature.sdkInitDate, backgroundEventTrackingEnabled: rumFeature.configuration.backgroundEventTrackingEnabled ), dateProvider: rumFeature.dateProvider diff --git a/Sources/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentation.swift b/Sources/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentation.swift index 6d2a7ff661..ba06f212ee 100644 --- a/Sources/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentation.swift +++ b/Sources/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentation.swift @@ -15,16 +15,15 @@ internal final class URLSessionAutoInstrumentation: RUMCommandPublisher { convenience init?( configuration: FeaturesConfiguration.URLSessionAutoInstrumentation, - dateProvider: DateProvider, - appStateListener: AppStateListening + commonDependencies: FeaturesCommonDependencies ) { do { self.init( swizzler: try URLSessionSwizzler(), interceptor: URLSessionInterceptor( configuration: configuration, - dateProvider: dateProvider, - appStateListener: appStateListener + dateProvider: commonDependencies.dateProvider, + appStateListener: commonDependencies.appStateListener ) ) } catch { diff --git a/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift b/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift index c594c007eb..cacfefc72d 100644 --- a/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift +++ b/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift @@ -23,12 +23,14 @@ extension FeaturesCommonDependencies { performance: .benchmarksPreset, httpClient: HTTPClient(), mobileDevice: MobileDevice(), + sdkInitDate: Date(), dateProvider: SystemDateProvider(), dateCorrector: DateCorrectorMock(), userInfoProvider: UserInfoProvider(), networkConnectionInfoProvider: NetworkConnectionInfoProvider(), carrierInfoProvider: CarrierInfoProvider(), - launchTimeProvider: LaunchTimeProvider() + launchTimeProvider: LaunchTimeProvider(), + appStateListener: AppStateListener(dateProvider: SystemDateProvider()) ) } } diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index 5d278c2f88..29b7cf4d85 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -454,6 +454,7 @@ extension FeaturesCommonDependencies { return BatteryStatus(state: .full, level: 1, isLowPowerModeEnabled: false) } ), + sdkInitDate: Date = Date(), dateProvider: DateProvider = SystemDateProvider(), dateCorrector: DateCorrectorType = DateCorrectorMock(), userInfoProvider: UserInfoProvider = .mockAny(), @@ -468,7 +469,8 @@ extension FeaturesCommonDependencies { ) ), carrierInfoProvider: CarrierInfoProviderType = CarrierInfoProviderMock.mockAny(), - launchTimeProvider: LaunchTimeProviderType = LaunchTimeProviderMock() + launchTimeProvider: LaunchTimeProviderType = LaunchTimeProviderMock(), + appStateListener: AppStateListening = AppStateListenerMock.mockAny() ) -> FeaturesCommonDependencies { let httpClient: HTTPClient @@ -496,12 +498,14 @@ extension FeaturesCommonDependencies { performance: performance, httpClient: httpClient, mobileDevice: mobileDevice, + sdkInitDate: sdkInitDate, dateProvider: dateProvider, dateCorrector: dateCorrector, userInfoProvider: userInfoProvider, networkConnectionInfoProvider: networkConnectionInfoProvider, carrierInfoProvider: carrierInfoProvider, - launchTimeProvider: launchTimeProvider + launchTimeProvider: launchTimeProvider, + appStateListener: appStateListener ) } @@ -511,24 +515,28 @@ extension FeaturesCommonDependencies { performance: PerformancePreset? = nil, httpClient: HTTPClient? = nil, mobileDevice: MobileDevice? = nil, + sdkInitDate: Date? = nil, dateProvider: DateProvider? = nil, dateCorrector: DateCorrectorType? = nil, userInfoProvider: UserInfoProvider? = nil, networkConnectionInfoProvider: NetworkConnectionInfoProviderType? = nil, carrierInfoProvider: CarrierInfoProviderType? = nil, - launchTimeProvider: LaunchTimeProviderType? = nil + launchTimeProvider: LaunchTimeProviderType? = nil, + appStateListener: AppStateListening? = nil ) -> FeaturesCommonDependencies { return FeaturesCommonDependencies( consentProvider: consentProvider ?? self.consentProvider, performance: performance ?? self.performance, httpClient: httpClient ?? self.httpClient, mobileDevice: mobileDevice ?? self.mobileDevice, + sdkInitDate: sdkInitDate ?? self.sdkInitDate, dateProvider: dateProvider ?? self.dateProvider, dateCorrector: dateCorrector ?? self.dateCorrector, userInfoProvider: userInfoProvider ?? self.userInfoProvider, networkConnectionInfoProvider: networkConnectionInfoProvider ?? self.networkConnectionInfoProvider, carrierInfoProvider: carrierInfoProvider ?? self.carrierInfoProvider, - launchTimeProvider: launchTimeProvider ?? self.launchTimeProvider + launchTimeProvider: launchTimeProvider ?? self.launchTimeProvider, + appStateListener: appStateListener ?? self.appStateListener ) } } @@ -655,6 +663,23 @@ struct LaunchTimeProviderMock: LaunchTimeProviderType { var launchTime: TimeInterval = 0 } +class AppStateListenerMock: AppStateListening, AnyMockable { + let history: AppStateHistory + + required init(history: AppStateHistory) { + self.history = history + } + + static func mockAny() -> Self { + return .init( + history: .init( + initialState: .init(isActive: true, date: .mockDecember15th2019At10AMUTC()), + recentDate: .mockDecember15th2019At10AMUTC() + ) + ) + } +} + extension UserInfo: AnyMockable, RandomMockable { static func mockAny() -> UserInfo { return mockEmpty() @@ -991,12 +1016,6 @@ class CarrierInfoProviderMock: CarrierInfoProviderType { } } -extension AppStateListener { - static func mockAny() -> AppStateListener { - return AppStateListener(dateProvider: SystemDateProvider()) - } -} - extension CodableValue { static func mockAny() -> CodableValue { return CodableValue(String.mockAny()) diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index b857c0e874..c4304ec7af 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -178,6 +178,15 @@ func mockRandomRUMCommand(where predicate: (RUMCommand) -> Bool = { _ in true }) return allCommands.filter(predicate).randomElement()! } +extension RUMCommand { + func replacing(time: Date? = nil, attributes: [AttributeKey: AttributeValue]? = nil) -> RUMCommand { + var command = self + command.time = time ?? command.time + command.attributes = attributes ?? command.attributes + return command + } +} + extension RUMStartViewCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStartViewCommand { mockWith() } @@ -596,6 +605,7 @@ extension RUMScopeDependencies { } static func mockWith( + appStateListener: AppStateListening = AppStateListenerMock.mockAny(), userInfoProvider: RUMUserInfoProvider = RUMUserInfoProvider(userInfoProvider: .mockAny()), launchTimeProvider: LaunchTimeProviderType = LaunchTimeProviderMock(), connectivityInfoProvider: RUMConnectivityInfoProvider = RUMConnectivityInfoProvider( @@ -609,6 +619,7 @@ extension RUMScopeDependencies { onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListerner() ) -> RUMScopeDependencies { return RUMScopeDependencies( + appStateListener: appStateListener, userInfoProvider: userInfoProvider, launchTimeProvider: launchTimeProvider, connectivityInfoProvider: connectivityInfoProvider, @@ -625,6 +636,7 @@ extension RUMScopeDependencies { /// Creates new instance of `RUMScopeDependencies` by replacing individual dependencies. func replacing( + appStateListener: AppStateListening? = nil, userInfoProvider: RUMUserInfoProvider? = nil, launchTimeProvider: LaunchTimeProviderType? = nil, connectivityInfoProvider: RUMConnectivityInfoProvider? = nil, @@ -635,6 +647,7 @@ extension RUMScopeDependencies { onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListerner() ) -> RUMScopeDependencies { return RUMScopeDependencies( + appStateListener: appStateListener ?? self.appStateListener, userInfoProvider: userInfoProvider ?? self.userInfoProvider, launchTimeProvider: launchTimeProvider ?? self.launchTimeProvider, connectivityInfoProvider: connectivityInfoProvider ?? self.connectivityInfoProvider, @@ -658,13 +671,15 @@ extension RUMApplicationScope { static func mockWith( rumApplicationID: String = .mockAny(), dependencies: RUMScopeDependencies = .mockAny(), - samplingRate: Float = 100, + samplingRate: Float = .mockAny(), + applicationStartTime: Date = .mockAny(), backgroundEventTrackingEnabled: Bool = .mockAny() ) -> RUMApplicationScope { return RUMApplicationScope( rumApplicationID: rumApplicationID, dependencies: dependencies, samplingRate: samplingRate, + applicationStartTime: applicationStartTime, backgroundEventTrackingEnabled: backgroundEventTrackingEnabled ) } diff --git a/Tests/DatadogTests/Datadog/RUM/Debugging/RUMDebuggingTests.swift b/Tests/DatadogTests/Datadog/RUM/Debugging/RUMDebuggingTests.swift index 9fcd133eb8..149a7870e8 100644 --- a/Tests/DatadogTests/Datadog/RUM/Debugging/RUMDebuggingTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/Debugging/RUMDebuggingTests.swift @@ -13,12 +13,7 @@ class RUMDebuggingTests: XCTestCase { let expectation = self.expectation(description: "Render RUMDebugging") // when - let applicationScope = RUMApplicationScope( - rumApplicationID: "abc-123", - dependencies: .mockAny(), - samplingRate: 100, - backgroundEventTrackingEnabled: .mockAny() - ) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 100) _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockView, name: "FirstView") ) @@ -45,12 +40,7 @@ class RUMDebuggingTests: XCTestCase { let expectation = self.expectation(description: "Render RUMDebugging") // when - let applicationScope = RUMApplicationScope( - rumApplicationID: "abc-123", - dependencies: .mockAny(), - samplingRate: 100, - backgroundEventTrackingEnabled: .mockAny() - ) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 100) _ = applicationScope.process( command: RUMStartViewCommand.mockWith(identity: mockView, name: "FirstView") ) diff --git a/Tests/DatadogTests/Datadog/RUM/RUMContext/RUMCurrentContextTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMContext/RUMCurrentContextTests.swift index e1da601b29..4050df60ac 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMContext/RUMCurrentContextTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMContext/RUMCurrentContextTests.swift @@ -13,7 +13,7 @@ class RUMCurrentContextTests: XCTestCase { private let queue = DispatchQueue(label: "\(#file)") func testContextAfterInitializingTheApplication() { - let applicationScope = RUMApplicationScope(rumApplicationID: "rum-123", dependencies: .mockAny(), samplingRate: .mockAny(), backgroundEventTrackingEnabled: .mockAny()) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") let provider = RUMCurrentContext(applicationScope: applicationScope, queue: queue) XCTAssertEqual( @@ -30,7 +30,7 @@ class RUMCurrentContextTests: XCTestCase { } func testContextAfterStartingView() throws { - let applicationScope = RUMApplicationScope(rumApplicationID: "rum-123", dependencies: .mockAny(), samplingRate: 100, backgroundEventTrackingEnabled: .mockAny()) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 100) let provider = RUMCurrentContext(applicationScope: applicationScope, queue: queue) _ = applicationScope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) @@ -49,7 +49,7 @@ class RUMCurrentContextTests: XCTestCase { } func testContextWhilePendingUserAction() throws { - let applicationScope = RUMApplicationScope(rumApplicationID: "rum-123", dependencies: .mockAny(), samplingRate: 100, backgroundEventTrackingEnabled: .mockAny()) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 100) let provider = RUMCurrentContext(applicationScope: applicationScope, queue: queue) _ = applicationScope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) @@ -69,7 +69,7 @@ class RUMCurrentContextTests: XCTestCase { } func testContextChangeWhenNavigatingBetweenViews() throws { - let applicationScope = RUMApplicationScope(rumApplicationID: "rum-123", dependencies: .mockAny(), samplingRate: 100, backgroundEventTrackingEnabled: .mockAny()) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 100) let provider = RUMCurrentContext(applicationScope: applicationScope, queue: queue) let firstView = createMockViewInWindow() @@ -97,7 +97,7 @@ class RUMCurrentContextTests: XCTestCase { func testContextChangeWhenSessionIsRenewed() throws { var currentTime = Date() - let applicationScope = RUMApplicationScope(rumApplicationID: "rum-123", dependencies: .mockAny(), samplingRate: 100, backgroundEventTrackingEnabled: .mockAny()) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 100) let provider = RUMCurrentContext(applicationScope: applicationScope, queue: queue) let view = createMockViewInWindow() @@ -140,7 +140,7 @@ class RUMCurrentContextTests: XCTestCase { } func testContextWhenSessionIsSampled() throws { - let applicationScope = RUMApplicationScope(rumApplicationID: "rum-123", dependencies: .mockAny(), samplingRate: 0, backgroundEventTrackingEnabled: .mockAny()) + let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123", samplingRate: 0) let provider = RUMCurrentContext(applicationScope: applicationScope, queue: queue) _ = applicationScope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift index e580766d2f..1446b42d4c 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift @@ -13,6 +13,7 @@ class RUMApplicationScopeTests: XCTestCase { rumApplicationID: "abc-123", dependencies: .mockAny(), samplingRate: .mockAny(), + applicationStartTime: .mockAny(), backgroundEventTrackingEnabled: .mockAny() ) @@ -31,19 +32,23 @@ class RUMApplicationScopeTests: XCTestCase { expectation.fulfill() } + // Given + let currentTime = Date() let scope = RUMApplicationScope( rumApplicationID: .mockAny(), - dependencies: .mockWith( - onSessionStart: onSessionStart - ), + dependencies: .mockWith(onSessionStart: onSessionStart), samplingRate: 0, + applicationStartTime: currentTime, backgroundEventTrackingEnabled: .mockRandom() ) - XCTAssertNil(scope.sessionScope) - XCTAssertTrue(scope.process(command: mockRandomRUMCommand())) + + // When + let command = mockRandomRUMCommand().replacing(time: currentTime.addingTimeInterval(1)) + XCTAssertTrue(scope.process(command: command)) waitForExpectations(timeout: 0.5) + // Then let sessionScope = try XCTUnwrap(scope.sessionScope) XCTAssertEqual(sessionScope.backgroundEventTrackingEnabled, scope.backgroundEventTrackingEnabled) XCTAssertTrue(sessionScope.isInitialSession, "Starting the very first view in application must create initial session") @@ -60,17 +65,15 @@ class RUMApplicationScopeTests: XCTestCase { } // Given + var currentTime = Date() let scope = RUMApplicationScope( rumApplicationID: .mockAny(), - dependencies: .mockWith( - onSessionStart: onSessionStart - ), + dependencies: .mockWith(onSessionStart: onSessionStart), samplingRate: 100, + applicationStartTime: currentTime, backgroundEventTrackingEnabled: .mockAny() ) - var currentTime = Date() - let view = createMockViewInWindow() _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: view)) @@ -101,10 +104,17 @@ class RUMApplicationScopeTests: XCTestCase { let output = RUMEventOutputMock() let dependencies: RUMScopeDependencies = .mockWith(eventOutput: output) - let scope = RUMApplicationScope(rumApplicationID: .mockAny(), dependencies: dependencies, samplingRate: 100, backgroundEventTrackingEnabled: .mockAny()) + let currentTime = Date() + let scope = RUMApplicationScope( + rumApplicationID: .mockAny(), + dependencies: dependencies, + samplingRate: 100, + applicationStartTime: currentTime, + backgroundEventTrackingEnabled: .mockAny() + ) - _ = scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) - _ = scope.process(command: RUMStopViewCommand.mockWith(identity: mockView)) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: mockView)) + _ = scope.process(command: RUMStopViewCommand.mockWith(time: currentTime, identity: mockView)) XCTAssertEqual(try output.recordedEvents(ofType: RUMEvent.self).count, 2) } @@ -113,10 +123,17 @@ class RUMApplicationScopeTests: XCTestCase { let output = RUMEventOutputMock() let dependencies: RUMScopeDependencies = .mockWith(eventOutput: output) - let scope = RUMApplicationScope(rumApplicationID: .mockAny(), dependencies: dependencies, samplingRate: 0, backgroundEventTrackingEnabled: .mockAny()) + let currentTime = Date() + let scope = RUMApplicationScope( + rumApplicationID: .mockAny(), + dependencies: dependencies, + samplingRate: 0, + applicationStartTime: currentTime, + backgroundEventTrackingEnabled: .mockAny() + ) - _ = scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) - _ = scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: mockView)) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: mockView)) XCTAssertEqual(try output.recordedEvents(ofType: RUMEvent.self).count, 0) } @@ -125,9 +142,15 @@ class RUMApplicationScopeTests: XCTestCase { let output = RUMEventOutputMock() let dependencies: RUMScopeDependencies = .mockWith(eventOutput: output) - let scope = RUMApplicationScope(rumApplicationID: .mockAny(), dependencies: dependencies, samplingRate: 50, backgroundEventTrackingEnabled: .mockAny()) - var currentTime = Date() + let scope = RUMApplicationScope( + rumApplicationID: .mockAny(), + dependencies: dependencies, + samplingRate: 50, + applicationStartTime: currentTime, + backgroundEventTrackingEnabled: .mockAny() + ) + let simulatedSessionsCount = 200 (0...self).last) + let firstActionEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).first) + XCTAssertEqual(lastViewEvent.model.view.action.count, 1, "View should record 1 only custom action (pending action is not yet finished)") + XCTAssertEqual(firstActionEvent.model.action.target?.name, customActionName) + } + + func testGivenViewWithNoPendingAction_whenCustomActionIsAdded_itSendsItInstantly() throws { + var currentTime = Date() + let scope = RUMViewScope( + isInitialView: false, + parent: parent, + dependencies: dependencies, + identity: mockView, + path: .mockAny(), + name: .mockAny(), + attributes: [:], + customTimings: [:], + startTime: currentTime + ) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: mockView)) + + // Given + currentTime.addTimeInterval(0.5) + + XCTAssertNil(scope.userActionScope) + + // When + let customActionName: String = .mockRandom() + _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime, actionType: .custom, name: customActionName)) + + // Then + XCTAssertNil(scope.userActionScope, "It should not count custom action as pending") + + let lastViewEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) + let firstActionEvent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).first) + XCTAssertEqual(lastViewEvent.model.view.action.count, 1, "View should record custom action") + XCTAssertEqual(firstActionEvent.model.action.target?.name, customActionName) + } + // MARK: - Error Tracking func testWhenViewErrorIsAdded_itSendsErrorEventAndViewUpdateEvent() throws { diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index ba25f52c50..0ef1c88f19 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -928,12 +928,15 @@ class RUMMonitorTests: XCTestCase { // MARK: - Tracking App Launch Events func testWhenCollectingEventsBeforeStartingFirstView_itTracksThemWithinApplicationLaunchView() throws { + let sdkInitDate: Date = .mockDecember15th2019At10AMUTC() + // Given RUMFeature.instance = .mockByRecordingRUMEventMatchers( directories: temporaryFeatureDirectories, dependencies: .mockWith( + sdkInitDate: sdkInitDate, dateProvider: RelativeDateProvider( - startingFrom: Date(), + startingFrom: sdkInitDate.addingTimeInterval(1), advancingBySeconds: 1 ) ) @@ -957,10 +960,15 @@ class RUMMonitorTests: XCTestCase { XCTAssertEqual(session.viewVisits.count, 2, "It should track 2 views") let appLaunchView = session.viewVisits[0] + let sdkInitDateInMilliseconds = sdkInitDate.timeIntervalSince1970.toInt64Milliseconds + XCTAssertEqual(appLaunchView.name, "ApplicationLaunch", "It should track 'ApplicationLaunch' view") + XCTAssertEqual(appLaunchView.viewEvents.first?.date, sdkInitDateInMilliseconds, "'ApplicationLaunch' view should start at SDK init") XCTAssertEqual(appLaunchView.actionEvents.count, 2, "'ApplicationLaunch' should track 2 actions") XCTAssertEqual(appLaunchView.actionEvents[0].action.type, .applicationStart, "'ApplicationLaunch' should track 'application start' action") + XCTAssertEqual(appLaunchView.actionEvents[0].date, sdkInitDateInMilliseconds, "'application start' action should be tracked at SDK init") XCTAssertEqual(appLaunchView.actionEvents[1].action.target?.name, "A1", "'ApplicationLaunch' should track 'A1' action") + XCTAssertGreaterThan(appLaunchView.actionEvents[1].date, sdkInitDateInMilliseconds, "'A1' action should be tracked after SDK init") XCTAssertEqual(appLaunchView.errorEvents.count, 1, "'ApplicationLaunch' should track 1 error") XCTAssertEqual(appLaunchView.errorEvents[0].error.message, "E1", "'ApplicationLaunch' should track 'E1' error") XCTAssertEqual(appLaunchView.resourceEvents.count, 1, "'ApplicationLaunch' should track 1 resource") diff --git a/Tests/DatadogTests/Datadog/Tracing/Autoinstrumentation/URLSessionTracingHandlerTests.swift b/Tests/DatadogTests/Datadog/Tracing/Autoinstrumentation/URLSessionTracingHandlerTests.swift index a6a8ff6b94..2c4242ed67 100644 --- a/Tests/DatadogTests/Datadog/Tracing/Autoinstrumentation/URLSessionTracingHandlerTests.swift +++ b/Tests/DatadogTests/Datadog/Tracing/Autoinstrumentation/URLSessionTracingHandlerTests.swift @@ -7,17 +7,17 @@ import XCTest @testable import Datadog -private class MockAppStateListener: AppStateListening { - let history = AppStateHistory( - initialState: .init(isActive: true, date: .mockDecember15th2019At10AMUTC()), - finalDate: .mockDecember15th2019At10AMUTC() + 10 - ) -} - class URLSessionTracingHandlerTests: XCTestCase { private let spanOutput = SpanOutputMock() private let logOutput = LogOutputMock() - private let handler = URLSessionTracingHandler(appStateListener: MockAppStateListener()) + private let handler = URLSessionTracingHandler( + appStateListener: AppStateListenerMock( + history: .init( + initialState: .init(isActive: true, date: .mockDecember15th2019At10AMUTC()), + recentDate: .mockDecember15th2019At10AMUTC() + 10 + ) + ) + ) override func setUp() { Global.sharedTracer = Tracer.mockWith( diff --git a/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/AppStateListenerTests.swift b/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/AppStateListenerTests.swift index 7af74b7cbd..ab228f8e61 100644 --- a/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/AppStateListenerTests.swift +++ b/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/AppStateListenerTests.swift @@ -13,7 +13,7 @@ class AppStateHistoryTests: XCTestCase { let history = AppStateHistory( initialState: .init(isActive: true, date: startDate), changes: [], - finalDate: startDate + 1.0 + recentDate: startDate + 1.0 ) XCTAssertEqual(history.foregroundDuration, 1.0) @@ -29,7 +29,7 @@ class AppStateHistoryTests: XCTestCase { .init(isActive: false, date: startDate + 3.0), .init(isActive: true, date: startDate + 4.0) ], - finalDate: startDate + 5.0 + recentDate: startDate + 5.0 ) XCTAssertEqual(history.foregroundDuration, 3.0) @@ -43,7 +43,7 @@ class AppStateHistoryTests: XCTestCase { .init(isActive: false, date: startDate + 1.0), .init(isActive: false, date: startDate + 3.0) ], - finalDate: startDate + 5.0 + recentDate: startDate + 5.0 ) XCTAssertEqual(history.foregroundDuration, 1.0) @@ -54,7 +54,7 @@ class AppStateHistoryTests: XCTestCase { let history = AppStateHistory( initialState: .init(isActive: true, date: startDate), changes: [], - finalDate: startDate + 5.0 + recentDate: startDate + 5.0 ) let extrapolatedHistory = history.take( between: (startDate - 5.0)...(startDate + 15.0) @@ -63,7 +63,7 @@ class AppStateHistoryTests: XCTestCase { let expectedHistory = AppStateHistory( initialState: .init(isActive: true, date: startDate - 5.0), changes: [], - finalDate: startDate + 15.0 + recentDate: startDate + 15.0 ) XCTAssertEqual(extrapolatedHistory, expectedHistory) } @@ -73,7 +73,7 @@ class AppStateHistoryTests: XCTestCase { let history = AppStateHistory( initialState: .init(isActive: true, date: startDate), changes: [], - finalDate: startDate + 20.0 + recentDate: startDate + 20.0 ) let limitedHistory = history.take( between: (startDate + 5.0)...(startDate + 10.0) @@ -82,7 +82,7 @@ class AppStateHistoryTests: XCTestCase { let expectedHistory = AppStateHistory( initialState: .init(isActive: true, date: startDate + 5.0), changes: [], - finalDate: startDate + 10.0 + recentDate: startDate + 10.0 ) XCTAssertEqual(limitedHistory, expectedHistory) } @@ -108,7 +108,7 @@ class AppStateHistoryTests: XCTestCase { let history = AppStateHistory( initialState: .init(isActive: true, date: startDate), changes: allChanges, - finalDate: startDate + 4_000 + recentDate: startDate + 4_000 ) let limitedHistory = history.take( @@ -118,7 +118,7 @@ class AppStateHistoryTests: XCTestCase { let expectedHistory = AppStateHistory( initialState: .init(isActive: true, date: startDate + 1_250), changes: [.init(isActive: false, date: startDate + 1_500)], - finalDate: startDate + 1_750 + recentDate: startDate + 1_750 ) XCTAssertEqual(limitedHistory, expectedHistory) } @@ -143,7 +143,7 @@ class AppStateListenerTests: XCTestCase { .init(isActive: false, date: startDate + 1.0), .init(isActive: true, date: startDate + 2.0) ], - finalDate: startDate + 3.0 + recentDate: startDate + 3.0 ) XCTAssertEqual(listener.history, expected) } @@ -163,7 +163,7 @@ class AppStateListenerTests: XCTestCase { .init(isActive: true, date: startDate + 1.0), .init(isActive: false, date: startDate + 2.0) ], - finalDate: startDate + 3.0 + recentDate: startDate + 3.0 ) XCTAssertEqual(listener.history, expected) } @@ -177,7 +177,7 @@ class AppStateListenerTests: XCTestCase { let history1 = listener.history let history2 = listener.history - XCTAssertEqual(history2.finalState.date.timeIntervalSince(history1.finalState.date), 1.0) + XCTAssertEqual(history2.currentState.date.timeIntervalSince(history1.currentState.date), 1.0) } func testWhenAppStateListenerIsCalledFromDifferentThreads_thenItWorks() { diff --git a/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/URLSessionInterceptorTests.swift b/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/URLSessionInterceptorTests.swift index 358847550c..94db9ad04b 100644 --- a/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/URLSessionInterceptorTests.swift +++ b/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/Interception/URLSessionInterceptorTests.swift @@ -26,7 +26,7 @@ class URLSessionInterceptorTests: XCTestCase { let instrumentRUM = false // When - let appStateListener = AppStateListener.mockAny() + let appStateListener = AppStateListenerMock.mockAny() let interceptor = URLSessionInterceptor( configuration: .mockWith(instrumentTracing: instrumentTracing, instrumentRUM: instrumentRUM), dateProvider: SystemDateProvider(), @@ -55,7 +55,7 @@ class URLSessionInterceptorTests: XCTestCase { let interceptor = URLSessionInterceptor( configuration: .mockWith(instrumentTracing: instrumentTracing, instrumentRUM: instrumentRUM), dateProvider: SystemDateProvider(), - appStateListener: AppStateListener.mockAny() + appStateListener: AppStateListenerMock.mockAny() ) // Then @@ -79,7 +79,7 @@ class URLSessionInterceptorTests: XCTestCase { let interceptor = URLSessionInterceptor( configuration: .mockWith(instrumentTracing: instrumentTracing, instrumentRUM: instrumentRUM), dateProvider: SystemDateProvider(), - appStateListener: AppStateListener.mockAny() + appStateListener: AppStateListenerMock.mockAny() ) // Then diff --git a/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentationTests.swift b/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentationTests.swift index d9834416b3..33ff6038e7 100644 --- a/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentationTests.swift +++ b/Tests/DatadogTests/Datadog/URLSessionAutoInstrumentation/URLSessionAutoInstrumentationTests.swift @@ -24,11 +24,9 @@ class URLSessionAutoInstrumentationTests: XCTestCase { // When URLSessionAutoInstrumentation.instance = URLSessionAutoInstrumentation( configuration: .mockAny(), - dateProvider: SystemDateProvider(), - appStateListener: AppStateListener.mockAny() + commonDependencies: .mockAny() ) defer { - URLSessionAutoInstrumentation.instance?.swizzler.unswizzle() URLSessionAutoInstrumentation.instance?.deinitialize() } @@ -43,11 +41,9 @@ class URLSessionAutoInstrumentationTests: XCTestCase { URLSessionAutoInstrumentation.instance = URLSessionAutoInstrumentation( configuration: .mockAny(), - dateProvider: SystemDateProvider(), - appStateListener: AppStateListener.mockAny() + commonDependencies: .mockAny() ) defer { - URLSessionAutoInstrumentation.instance?.swizzler.unswizzle() URLSessionAutoInstrumentation.instance?.deinitialize() }