From 098909da66b44c9365f10353afcae106305ccba8 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 29 Nov 2021 12:21:26 +0100 Subject: [PATCH 1/6] RUMM-1765 Make starting "Background" view based on the command's property It also fixes the bug where existing, but inactive view scope was preventing "Background" view from being started. This was corresponding to the scenario when an app is sent to background while a resource is being loaded - until the resource finished, no "Background" view could be started. --- Sources/Datadog/DatadogConfiguration.swift | 2 +- .../Datadog/RUM/RUMMonitor/RUMCommand.swift | 16 ++ .../RUMMonitor/Scopes/RUMSessionScope.swift | 53 +++--- .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 3 - .../Datadog/Mocks/RUMFeatureMocks.swift | 1 + .../Scopes/RUMSessionScopeTests.swift | 177 ++++++++---------- 6 files changed, 128 insertions(+), 124 deletions(-) diff --git a/Sources/Datadog/DatadogConfiguration.swift b/Sources/Datadog/DatadogConfiguration.swift index f66f4aad98..c018613ac2 100644 --- a/Sources/Datadog/DatadogConfiguration.swift +++ b/Sources/Datadog/DatadogConfiguration.swift @@ -675,7 +675,7 @@ extension Datadog { return self } - /// Enables or disables automatic tracking of background events (events hapenning when no UIViewController is active). + /// Enables or disables automatic tracking of background events (events hapenning when no `UIViewController` is active). /// /// When enabled, the SDK will track RUM Events into an automatically created Background RUM View (named `Background`) /// diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index d9e9f88728..8bb69aa451 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -12,6 +12,10 @@ internal protocol RUMCommand { var time: Date { set get } /// Attributes associated with the command. var attributes: [AttributeKey: AttributeValue] { set get } + + /// Whether or not receiving this command should start the "Background" view if no view is active + /// and ``Datadog.Configuration.Builder.trackBackgroundEvents(_:)`` is enabled. + var canStartBackgroundView: Bool { get } } // MARK: - RUM View related commands @@ -19,6 +23,7 @@ internal protocol RUMCommand { internal struct RUMStartViewCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, it should start its own view, not the "Background" /// The value holding stable identity of the RUM View. let identity: RUMViewIdentifiable @@ -52,6 +57,7 @@ internal struct RUMStartViewCommand: RUMCommand { internal struct RUMStopViewCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, we don't expect receiving it without an active view /// The value holding stable identity of the RUM View. let identity: RUMViewIdentifiable @@ -60,6 +66,7 @@ internal struct RUMStopViewCommand: RUMCommand { internal struct RUMAddCurrentViewErrorCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = true // yes, we want to track errors in "Background" view /// The error message. let message: String @@ -116,6 +123,7 @@ internal struct RUMAddCurrentViewErrorCommand: RUMCommand { internal struct RUMAddViewTimingCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, it doesn't make sense to start "Background" view on receiving custom timing, as it will be `0ns` timing /// The name of the timing. It will be used as a JSON key, whereas the value will be the timing duration, /// measured since the start of the View. @@ -140,6 +148,7 @@ internal struct RUMStartResourceCommand: RUMResourceCommand { let resourceKey: String var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = true // yes, we want to track resources in "Background" view /// Resource url let url: String @@ -157,6 +166,7 @@ internal struct RUMAddResourceMetricsCommand: RUMResourceCommand { let resourceKey: String var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) /// Resource metrics. let metrics: ResourceMetrics @@ -166,6 +176,7 @@ internal struct RUMStopResourceCommand: RUMResourceCommand { let resourceKey: String var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) /// A type of the Resource let kind: RUMResourceType @@ -179,6 +190,7 @@ internal struct RUMStopResourceWithErrorCommand: RUMResourceCommand { let resourceKey: String var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) /// The error message. let errorMessage: String @@ -250,6 +262,7 @@ internal protocol RUMUserActionCommand: RUMCommand { internal struct RUMStartUserActionCommand: RUMUserActionCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = true // yes, we want to track actions in "Background" view (e.g. it makes sense for custom actions) let actionType: RUMUserActionType let name: String @@ -259,6 +272,7 @@ internal struct RUMStartUserActionCommand: RUMUserActionCommand { internal struct RUMStopUserActionCommand: RUMUserActionCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartUserActionCommand`) let actionType: RUMUserActionType let name: String? @@ -268,6 +282,7 @@ internal struct RUMStopUserActionCommand: RUMUserActionCommand { internal struct RUMAddUserActionCommand: RUMUserActionCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = true // yes, we want to track actions in "Background" view (e.g. it makes sense for custom actions) let actionType: RUMUserActionType let name: String @@ -278,6 +293,7 @@ internal struct RUMAddUserActionCommand: RUMUserActionCommand { internal struct RUMAddLongTaskCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false // no, we don't expect receiving long tasks in "Background" view let duration: TimeInterval } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index fe71d769d9..4b700f6bbc 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -13,6 +13,10 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { static let sessionTimeoutDuration: TimeInterval = 15 * 60 // 15 minutes /// Maximum duration of a session. If it gets exceeded, a new session is started. static let sessionMaxDuration: TimeInterval = 4 * 60 * 60 // 4 hours + /// The name of a view created when receiving an event while there is no active view and background events tracking is enabled. + static let backgroundViewName = "Background" + /// The url of a view created when receiving an event while there is no active view and background events tracking is enabled. + static let backgroundViewURL = "com/datadog/background/view" } // MARK: - Child Scopes @@ -25,7 +29,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { unowned let parent: RUMContextProvider private let dependencies: RUMScopeDependencies - /// Automatically detect background events + /// Automatically detect background events by creating "Background" view if no other view is active internal let backgroundEventTrackingEnabled: Bool /// This Session UUID. Equals `.nullUUID` if the Session is sampled. @@ -107,14 +111,13 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { return true } - // Apply side effects - switch command { - case let command as RUMStartViewCommand: - startView(on: command) - case is RUMStartResourceCommand, is RUMAddUserActionCommand, is RUMStartUserActionCommand: - handleOrphanStartCommand(command: command) - default: - break + // Start an active view if necessary + if let startViewCommand = command as? RUMStartViewCommand { + startView(on: startViewCommand) + } else if !hasActiveView { + if backgroundEventTrackingEnabled && command.canStartBackgroundView { + startBackgroundView(on: command) + } } // Propagate command @@ -133,6 +136,11 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { return true } + /// If there is an active view, which can receive commands. + private var hasActiveView: Bool { + return viewScopes.last?.isActiveView ?? false + } + // MARK: - RUMCommands Processing private func startView(on command: RUMStartViewCommand) { @@ -150,22 +158,19 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { ) } - // MARK: - Private - private func handleOrphanStartCommand(command: RUMCommand) { - if viewScopes.isEmpty && backgroundEventTrackingEnabled { - viewScopes.append( - RUMViewScope( - parent: self, - dependencies: dependencies, - identity: RUMViewScope.Constants.backgroundViewURL, - path: RUMViewScope.Constants.backgroundViewURL, - name: RUMViewScope.Constants.backgroundViewName, - attributes: command.attributes, - customTimings: [:], - startTime: command.time - ) + private func startBackgroundView(on command: RUMCommand) { + viewScopes.append( + RUMViewScope( + parent: self, + dependencies: dependencies, + identity: Constants.backgroundViewURL, + path: Constants.backgroundViewURL, + name: Constants.backgroundViewName, + attributes: command.attributes, + customTimings: [:], + startTime: command.time ) - } + ) } private func timedOutOrExpired(currentTime: Date) -> Bool { diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index d7358c2c15..aca25573d7 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -8,9 +8,6 @@ import Foundation internal class RUMViewScope: RUMScope, RUMContextProvider { struct Constants { - static let backgroundViewURL = "com/datadog/background/view" - static let backgroundViewName = "Background" - static let frozenFrameThresholdInNs = (0.07).toInt64Nanoseconds // 70ms static let slowRenderingThresholdFPS = 55.0 } diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index a548fbd6d2..b074e01175 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -155,6 +155,7 @@ extension RUMEventsMapper { struct RUMCommandMock: RUMCommand { var time = Date() var attributes: [AttributeKey: AttributeValue] = [:] + var canStartBackgroundView = false } extension RUMStartViewCommand { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 0659bb918f..2b6376bd70 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -79,119 +79,112 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual(scope.viewScopes.count, 0) } - func testWhenNoViewScope_andReceivedStartResourceCommand_itCreatesNewViewScope() { - let parent = RUMContextProviderMock() - let currentTime = Date() - - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: true) - - let previousUserLogger = userLogger - defer { userLogger = previousUserLogger } - - let logOutput = LogOutputMock() - userLogger = .mockWith(logOutput: logOutput) - - _ = scope.process(command: RUMStartResourceCommand.mockWith(resourceKey: "/resource/1", time: currentTime)) + // MARK: - Background Events Tracking - XCTAssertEqual(scope.viewScopes.count, 1) - XCTAssertNil(logOutput.recordedLog?.message) - XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) - XCTAssertEqual(scope.viewScopes[0].viewName, RUMViewScope.Constants.backgroundViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMViewScope.Constants.backgroundViewURL) - } - - func testWhenNoViewScope_andReceivedStartActionCommand_itCreatesNewViewScope() { - let parent = RUMContextProviderMock() + func testGivenNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { + // Given let currentTime = Date() + let scope: RUMSessionScope = .mockWith( + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: true + ) + XCTAssertTrue(scope.viewScopes.isEmpty) - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: true) - - let previousUserLogger = userLogger - defer { userLogger = previousUserLogger } - - let logOutput = LogOutputMock() - userLogger = .mockWith(logOutput: logOutput) - - _ = scope.process(command: RUMStartUserActionCommand.mockWith(time: currentTime)) + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true) + XCTAssertTrue(scope.process(command: command)) - XCTAssertEqual(scope.viewScopes.count, 1) - XCTAssertNil(logOutput.recordedLog?.message) + // Then + XCTAssertEqual(scope.viewScopes.count, 1, "It should start background view scope") XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) - XCTAssertEqual(scope.viewScopes[0].viewName, RUMViewScope.Constants.backgroundViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMViewScope.Constants.backgroundViewURL) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMSessionScope.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMSessionScope.Constants.backgroundViewURL) } - func testWhenNoViewScope_andReceivedAddUserActionCommand_itCreatesNewViewScope() { - let parent = RUMContextProviderMock() + func testGivenNoActiveViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { + // Given let currentTime = Date() + let parent: RUMApplicationScope = .mockAny() + let scope: RUMSessionScope = .mockWith( + parent: parent, + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: true + ) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: "view")) + _ = scope.process(command: RUMStartResourceCommand.mockAny()) + _ = scope.process(command: RUMStopViewCommand.mockWith(time: currentTime.addingTimeInterval(1), identity: "view")) - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: true) - - let previousUserLogger = userLogger - defer { userLogger = previousUserLogger } - - let logOutput = LogOutputMock() - userLogger = .mockWith(logOutput: logOutput) + XCTAssertEqual(scope.viewScopes.count, 1, "It has one view scope...") + XCTAssertFalse(scope.viewScopes[0].isActiveView, "... but the view is not active") - _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime)) + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true) + XCTAssertTrue(scope.process(command: command)) - XCTAssertEqual(scope.viewScopes.count, 1) - XCTAssertNil(logOutput.recordedLog?.message) - XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) - XCTAssertEqual(scope.viewScopes[0].viewName, RUMViewScope.Constants.backgroundViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMViewScope.Constants.backgroundViewURL) + // Then + XCTAssertEqual(scope.viewScopes.count, 2, "It should start background view scope") + XCTAssertEqual(scope.viewScopes[1].viewStartTime, currentTime) + XCTAssertEqual(scope.viewScopes[1].viewName, RUMSessionScope.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[1].viewPath, RUMSessionScope.Constants.backgroundViewURL) } - func testWhenNoViewScope_andReceivedStartResourceCommand_andBackgroundDisabled_itDoesNotCreateNewViewScope() { - let parent = RUMContextProviderMock() + func testGivenNoViewScopeAndBackgroundEventsTrackingDisabled_whenCommandCanStartBackgroundView_itDoesNotCreateBackgroundScope() { + // Given let currentTime = Date() + let scope: RUMSessionScope = .mockWith( + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: false + ) + XCTAssertTrue(scope.viewScopes.isEmpty) - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: false) - - _ = scope.process(command: RUMStartResourceCommand.mockWith(resourceKey: "/resource/1", time: currentTime)) + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true) + XCTAssertTrue(scope.process(command: command)) - XCTAssertEqual(scope.viewScopes.count, 0) + // Then + XCTAssertTrue(scope.viewScopes.isEmpty, "It shoul not start any view scope") } - func testWhenNoViewScope_andReceivedStartActionCommand_andBackgroundDisabled_itDoesNotCreateNewViewScope() { - let parent = RUMContextProviderMock() + func testGivenNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { + // Given let currentTime = Date() + let scope: RUMSessionScope = .mockWith( + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: true + ) + XCTAssertTrue(scope.viewScopes.isEmpty) - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: false) - - _ = scope.process(command: RUMStartUserActionCommand.mockWith(time: currentTime)) + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false) + XCTAssertTrue(scope.process(command: command)) - XCTAssertEqual(scope.viewScopes.count, 0) + // Then + XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") } - func testWhenNoViewScope_andReceivedAddUserActionCommand_andBackgroundDisabled_itDoesNotCreateNewViewScope() { - let parent = RUMContextProviderMock() + func testGivenNoViewScopeAndBackgroundEventsTrackingDisabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { + // Given let currentTime = Date() + let scope: RUMSessionScope = .mockWith( + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: false + ) + XCTAssertTrue(scope.viewScopes.isEmpty) - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: false) - - _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime)) - - XCTAssertEqual(scope.viewScopes.count, 0) - } - - func testWhenActiveViewScope_andReceivingStartCommand_itDoesNotCreateNewViewScope() { - let parent = RUMContextProviderMock() - let currentTime = Date() + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false) + XCTAssertTrue(scope.process(command: command)) - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: .mockAny()) - _ = scope.process(command: generateRandomNotValidStartCommand()) - _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime)) - XCTAssertEqual(scope.viewScopes.count, 0) + // Then + XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") } - func testWhenNoActiveViewScope_andReceivingNotValidStartCommand_itDoesNotCreateNewViewScope() { - let parent = RUMContextProviderMock() - - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: .mockAny()) - _ = scope.process(command: generateRandomNotValidStartCommand()) - XCTAssertEqual(scope.viewScopes.count, 0) - } + // MARK: - Sampling func testWhenSessionIsSampled_itDoesNotCreateViewScopes() { let parent = RUMContextProviderMock() @@ -205,6 +198,8 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual(scope.viewScopes.count, 0) } + // MARK: - Usage + func testWhenNoActiveViewScopes_itLogsWarning() { // Given let parent = RUMContextProviderMock() @@ -218,7 +213,7 @@ class RUMSessionScopeTests: XCTestCase { let logOutput = LogOutputMock() userLogger = .mockWith(logOutput: logOutput) - let command = generateRandomNotValidStartCommand() + let command = RUMCommandMock(time: Date(), canStartBackgroundView: false) // When _ = scope.process(command: command) @@ -234,14 +229,4 @@ class RUMSessionScopeTests: XCTestCase { """ ) } - - // MARK: - Private - - private func generateRandomValidStartCommand() -> RUMCommand { - return [RUMStartUserActionCommand.mockAny(), RUMStartResourceCommand.mockAny(), RUMAddUserActionCommand.mockAny()].randomElement()! - } - - private func generateRandomNotValidStartCommand() -> RUMCommand { - return [RUMStopViewCommand.mockAny(), RUMStopResourceCommand.mockAny(), RUMStopUserActionCommand.mockAny(), RUMAddCurrentViewErrorCommand.mockWithErrorObject()].randomElement()! - } } From 414ec7146c6b9c487288a21135018e57c954e2ec Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Mon, 29 Nov 2021 14:32:57 +0100 Subject: [PATCH 2/6] RUMM-1765 Mark the very first RUM session in the process as "initial" --- .../Scopes/RUMApplicationScope.swift | 1 + .../RUMMonitor/Scopes/RUMSessionScope.swift | 5 +++ .../Datadog/Mocks/RUMFeatureMocks.swift | 4 ++- .../Scopes/RUMApplicationScopeTests.swift | 34 ++++++++++++------- .../Scopes/RUMSessionScopeTests.swift | 14 ++++---- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index ea4a35826e..190ac1cd71 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -101,6 +101,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { startInitialViewCommand.isInitialView = true let initialSession = RUMSessionScope( + isInitialSession: true, parent: self, dependencies: dependencies, samplingRate: samplingRate, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 4b700f6bbc..b7081f304f 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -36,6 +36,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { let sessionUUID: RUMUUID /// Tells if events from this Session should be sampled-out (not send). let shouldBeSampledOut: Bool + /// If this is the very first session created in the current app process (`false` for session created upon expiration of a previous one). + let isInitialSession: Bool /// RUM Session sampling rate. private let samplingRate: Float /// The start time of this Session. @@ -44,6 +46,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { private var lastInteractionTime: Date init( + isInitialSession: Bool, parent: RUMContextProvider, dependencies: RUMScopeDependencies, samplingRate: Float, @@ -55,6 +58,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { self.samplingRate = samplingRate self.shouldBeSampledOut = RUMSessionScope.randomizeSampling(using: samplingRate) self.sessionUUID = shouldBeSampledOut ? .nullUUID : dependencies.rumUUIDGenerator.generateUnique() + self.isInitialSession = isInitialSession self.sessionStartTime = startTime self.lastInteractionTime = startTime self.backgroundEventTrackingEnabled = backgroundEventTrackingEnabled @@ -66,6 +70,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { startTime: Date ) { self.init( + isInitialSession: false, parent: expiredSession.parent, dependencies: expiredSession.dependencies, samplingRate: expiredSession.samplingRate, diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index b074e01175..e0832db263 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -474,13 +474,15 @@ extension RUMSessionScope { } static func mockWith( - parent: RUMApplicationScope = .mockAny(), + isInitialSession: Bool = .mockAny(), + parent: RUMContextProvider = RUMContextProviderMock(), dependencies: RUMScopeDependencies = .mockAny(), samplingRate: Float = 100, startTime: Date = .mockAny(), backgroundEventTrackingEnabled: Bool = .mockAny() ) -> RUMSessionScope { return RUMSessionScope( + isInitialSession: isInitialSession, parent: parent, dependencies: dependencies, samplingRate: samplingRate, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift index b73e906a30..523252f237 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift @@ -23,7 +23,7 @@ class RUMApplicationScopeTests: XCTestCase { XCTAssertNil(scope.context.activeUserActionID) } - func testWhenFirstViewIsStarted_itStartsNewSession() { + func testWhenFirstViewIsStarted_itStartsNewSession() throws { let expectation = self.expectation(description: "onSessionStart is called") let onSessionStart: RUMSessionListener = { sessionId, isDiscarded in XCTAssertTrue(sessionId.matches(regex: .uuidRegex)) @@ -37,14 +37,16 @@ class RUMApplicationScopeTests: XCTestCase { onSessionStart: onSessionStart ), samplingRate: 0, - backgroundEventTrackingEnabled: .mockAny() + backgroundEventTrackingEnabled: .mockRandom() ) XCTAssertNil(scope.sessionScope) XCTAssertTrue(scope.process(command: RUMStartViewCommand.mockAny())) waitForExpectations(timeout: 0.5) - XCTAssertNotNil(scope.sessionScope) - XCTAssertEqual(scope.sessionScope?.backgroundEventTrackingEnabled, scope.backgroundEventTrackingEnabled) + + 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") } func testWhenSessionExpires_itStartsANewOneAndTransfersActiveViews() throws { @@ -57,6 +59,7 @@ class RUMApplicationScopeTests: XCTestCase { expectation.fulfill() } + // Given let scope = RUMApplicationScope( rumApplicationID: .mockAny(), dependencies: .mockWith( @@ -70,21 +73,26 @@ class RUMApplicationScopeTests: XCTestCase { let view = createMockViewInWindow() _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: view)) - let firstSessionUUID = try XCTUnwrap(scope.sessionScope?.context.sessionID) - let firstsSessionViewScopes = try XCTUnwrap(scope.sessionScope?.viewScopes) + let initialSession = try XCTUnwrap(scope.sessionScope) + + // When // Push time forward by the max session duration: currentTime.addTimeInterval(RUMSessionScope.Constants.sessionMaxDuration) - _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime)) - let secondSessionUUID = try XCTUnwrap(scope.sessionScope?.context.sessionID) - let secondSessionViewScopes = try XCTUnwrap(scope.sessionScope?.viewScopes) - let secondSessionViewScope = try XCTUnwrap(secondSessionViewScopes.first) + // Then waitForExpectations(timeout: 0.5) - XCTAssertNotEqual(firstSessionUUID, secondSessionUUID) - XCTAssertEqual(firstsSessionViewScopes.count, secondSessionViewScopes.count) - XCTAssertTrue(secondSessionViewScope.identity.equals(view)) + + let nextSession = try XCTUnwrap(scope.sessionScope) + XCTAssertNotEqual(initialSession.sessionUUID, nextSession.sessionUUID, "New session must have different id") + XCTAssertEqual(initialSession.viewScopes.count, nextSession.viewScopes.count, "All view scopes must be transferred to the new session") + + let initialViewScope = try XCTUnwrap(initialSession.viewScopes.first) + let transferredViewScope = try XCTUnwrap(nextSession.viewScopes.first) + XCTAssertNotEqual(initialViewScope.viewUUID, transferredViewScope.viewUUID, "Transferred view scope must have different view id") + XCTAssertTrue(transferredViewScope.identity.equals(view), "Transferred view scope must track the same view") + XCTAssertFalse(nextSession.isInitialSession, "Any next session in the application must be marked as 'not initial'") } func testUntilSessionIsStarted_itIgnoresOtherCommands() { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 2b6376bd70..4e3651d408 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -12,7 +12,7 @@ class RUMSessionScopeTests: XCTestCase { func testDefaultContext() { let parent: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: .mockAny(), backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 100) XCTAssertEqual(scope.context.rumApplicationID, "rum-123") XCTAssertNotEqual(scope.context.sessionID, .nullUUID) @@ -23,7 +23,7 @@ class RUMSessionScopeTests: XCTestCase { func testContextWhenSessionIsSampled() { let parent: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 0, startTime: .mockAny(), backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 0) XCTAssertEqual(scope.context.rumApplicationID, "rum-123") XCTAssertEqual(scope.context.sessionID, .nullUUID) @@ -35,7 +35,7 @@ class RUMSessionScopeTests: XCTestCase { func testWhenSessionExceedsMaxDuration_itGetsClosed() { var currentTime = Date() let parent = RUMContextProviderMock() - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 50, startTime: currentTime, backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 50, startTime: currentTime) XCTAssertTrue(scope.process(command: RUMCommandMock(time: currentTime))) @@ -48,7 +48,7 @@ class RUMSessionScopeTests: XCTestCase { func testWhenSessionIsInactiveForCertainDuration_itGetsClosed() { var currentTime = Date() let parent = RUMContextProviderMock() - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 50, startTime: currentTime, backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 50, startTime: currentTime) XCTAssertTrue(scope.process(command: RUMCommandMock(time: currentTime))) @@ -66,7 +66,7 @@ class RUMSessionScopeTests: XCTestCase { func testItManagesViewScopeLifecycle() { let parent = RUMContextProviderMock() - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 100, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) _ = scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) XCTAssertEqual(scope.viewScopes.count, 1) @@ -189,7 +189,7 @@ class RUMSessionScopeTests: XCTestCase { func testWhenSessionIsSampled_itDoesNotCreateViewScopes() { let parent = RUMContextProviderMock() - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 0, startTime: Date(), backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 0, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) XCTAssertTrue( scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)), @@ -204,7 +204,7 @@ class RUMSessionScopeTests: XCTestCase { // Given let parent = RUMContextProviderMock() - let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date(), backgroundEventTrackingEnabled: .mockAny()) + let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 100, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) let previousUserLogger = userLogger From 7e7daed0aa278bd29e47b0f39c9562ba57f84868 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 30 Nov 2021 13:20:10 +0100 Subject: [PATCH 3/6] RUMM-1765 Handle application launch events in `RUMSessionScope` --- .../Datadog/RUM/RUMMonitor/RUMCommand.swift | 15 ++- .../RUMMonitor/Scopes/RUMSessionScope.swift | 41 ++++++-- .../Datadog/Mocks/RUMFeatureMocks.swift | 1 + .../Scopes/RUMSessionScopeTests.swift | 93 ++++++++++++++----- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index 8bb69aa451..97c9aa95cf 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -12,10 +12,11 @@ internal protocol RUMCommand { var time: Date { set get } /// Attributes associated with the command. var attributes: [AttributeKey: AttributeValue] { set get } - /// Whether or not receiving this command should start the "Background" view if no view is active /// and ``Datadog.Configuration.Builder.trackBackgroundEvents(_:)`` is enabled. var canStartBackgroundView: Bool { get } + /// Whether or not receiving this command should start the "ApplicationLaunch" view if no view was yet started in current app process. + var canStartApplicationLaunchView: Bool { get } } // MARK: - RUM View related commands @@ -24,6 +25,7 @@ internal struct RUMStartViewCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, it should start its own view, not the "Background" + let canStartApplicationLaunchView = false // no, it should start its own view, not the "ApplicationLaunch" /// The value holding stable identity of the RUM View. let identity: RUMViewIdentifiable @@ -58,6 +60,7 @@ internal struct RUMStopViewCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, we don't expect receiving it without an active view + let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view /// The value holding stable identity of the RUM View. let identity: RUMViewIdentifiable @@ -67,6 +70,7 @@ internal struct RUMAddCurrentViewErrorCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = true // yes, we want to track errors in "Background" view + let canStartApplicationLaunchView = true // yes, we want to track errors in "ApplicationLaunch" view /// The error message. let message: String @@ -124,6 +128,7 @@ internal struct RUMAddViewTimingCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, it doesn't make sense to start "Background" view on receiving custom timing, as it will be `0ns` timing + let canStartApplicationLaunchView = false // no, it doesn't make sense to start "ApplicationLaunch" view on receiving custom timing, as it will be `0ns` timing /// The name of the timing. It will be used as a JSON key, whereas the value will be the timing duration, /// measured since the start of the View. @@ -149,6 +154,7 @@ internal struct RUMStartResourceCommand: RUMResourceCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = true // yes, we want to track resources in "Background" view + let canStartApplicationLaunchView = true // yes, we want to track resources in "ApplicationLaunch" view /// Resource url let url: String @@ -167,6 +173,7 @@ internal struct RUMAddResourceMetricsCommand: RUMResourceCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) + let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) /// Resource metrics. let metrics: ResourceMetrics @@ -177,6 +184,7 @@ internal struct RUMStopResourceCommand: RUMResourceCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) + let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) /// A type of the Resource let kind: RUMResourceType @@ -191,6 +199,7 @@ internal struct RUMStopResourceWithErrorCommand: RUMResourceCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) + let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`) /// The error message. let errorMessage: String @@ -263,6 +272,7 @@ internal struct RUMStartUserActionCommand: RUMUserActionCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = true // yes, we want to track actions in "Background" view (e.g. it makes sense for custom actions) + let canStartApplicationLaunchView = true // yes, we want to track actions in "ApplicationLaunch" view (e.g. it makes sense for custom actions) let actionType: RUMUserActionType let name: String @@ -273,6 +283,7 @@ internal struct RUMStopUserActionCommand: RUMUserActionCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartUserActionCommand`) + let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartUserActionCommand`) let actionType: RUMUserActionType let name: String? @@ -283,6 +294,7 @@ internal struct RUMAddUserActionCommand: RUMUserActionCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = true // yes, we want to track actions in "Background" view (e.g. it makes sense for custom actions) + let canStartApplicationLaunchView = true // yes, we want to track actions in "ApplicationLaunch" view (e.g. it makes sense for custom actions) let actionType: RUMUserActionType let name: String @@ -294,6 +306,7 @@ internal struct RUMAddLongTaskCommand: RUMCommand { var time: Date var attributes: [AttributeKey: AttributeValue] let canStartBackgroundView = false // no, we don't expect receiving long tasks in "Background" view + let canStartApplicationLaunchView = true // yes, we want to track long tasks in "ApplicationLaunch" view (e.g. any hitches before presenting first UI) let duration: TimeInterval } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index b7081f304f..acfdd7a572 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -17,12 +17,22 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { static let backgroundViewName = "Background" /// The url of a view created when receiving an event while there is no active view and background events tracking is enabled. static let backgroundViewURL = "com/datadog/background/view" + /// The name of a view created when receiving an event before any view was started in the initial session. + static let applicationLaunchViewName = "ApplicationLaunch" + /// The url of a view created when receiving an event before any view was started in the initial session. + static let applicationLaunchViewURL = "com/datadog/application-launch/view" } // MARK: - Child Scopes /// Active View scopes. Scopes are added / removed when the View starts / stops displaying. - private(set) var viewScopes: [RUMViewScope] = [] + private(set) var viewScopes: [RUMViewScope] = [] { + didSet { + hasTrackedAnyView = hasTrackedAnyView || !viewScopes.isEmpty + } + } + /// If this session has ever tracked any view. + private var hasTrackedAnyView = false // MARK: - Initialization @@ -116,13 +126,13 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { return true } - // Start an active view if necessary + // Consider starting an active view, "ApplicationLaunch" view or "Background" view if let startViewCommand = command as? RUMStartViewCommand { startView(on: startViewCommand) - } else if !hasActiveView { - if backgroundEventTrackingEnabled && command.canStartBackgroundView { - startBackgroundView(on: command) - } + } else if isInitialSession && !hasTrackedAnyView && command.canStartApplicationLaunchView { + startApplicationLaunchView(on: command) + } else if backgroundEventTrackingEnabled && !hasActiveView && command.canStartBackgroundView { + startBackgroundView(on: command) } // Propagate command @@ -141,9 +151,9 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { return true } - /// If there is an active view, which can receive commands. + /// If there is an active view. private var hasActiveView: Bool { - return viewScopes.last?.isActiveView ?? false + return viewScopes.contains { $0.isActiveView } } // MARK: - RUMCommands Processing @@ -163,6 +173,21 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { ) } + private func startApplicationLaunchView(on command: RUMCommand) { + viewScopes.append( + RUMViewScope( + parent: self, + dependencies: dependencies, + identity: Constants.applicationLaunchViewURL, + path: Constants.applicationLaunchViewURL, + name: Constants.applicationLaunchViewName, + attributes: command.attributes, + customTimings: [:], + startTime: command.time + ) + ) + } + private func startBackgroundView(on command: RUMCommand) { viewScopes.append( RUMViewScope( diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index e0832db263..4a939a7456 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -156,6 +156,7 @@ struct RUMCommandMock: RUMCommand { var time = Date() var attributes: [AttributeKey: AttributeValue] = [:] var canStartBackgroundView = false + var canStartApplicationLaunchView = false } extension RUMStartViewCommand { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index 4e3651d408..26a2b8d5ed 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -8,10 +8,11 @@ import XCTest @testable import Datadog class RUMSessionScopeTests: XCTestCase { + private let parent: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") + // MARK: - Unit Tests func testDefaultContext() { - let parent: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 100) XCTAssertEqual(scope.context.rumApplicationID, "rum-123") @@ -22,7 +23,6 @@ class RUMSessionScopeTests: XCTestCase { } func testContextWhenSessionIsSampled() { - let parent: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 0) XCTAssertEqual(scope.context.rumApplicationID, "rum-123") @@ -34,7 +34,6 @@ class RUMSessionScopeTests: XCTestCase { func testWhenSessionExceedsMaxDuration_itGetsClosed() { var currentTime = Date() - let parent = RUMContextProviderMock() let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 50, startTime: currentTime) XCTAssertTrue(scope.process(command: RUMCommandMock(time: currentTime))) @@ -47,7 +46,6 @@ class RUMSessionScopeTests: XCTestCase { func testWhenSessionIsInactiveForCertainDuration_itGetsClosed() { var currentTime = Date() - let parent = RUMContextProviderMock() let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 50, startTime: currentTime) XCTAssertTrue(scope.process(command: RUMCommandMock(time: currentTime))) @@ -64,8 +62,6 @@ class RUMSessionScopeTests: XCTestCase { } func testItManagesViewScopeLifecycle() { - let parent = RUMContextProviderMock() - let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 100, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) _ = scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) @@ -85,6 +81,8 @@ class RUMSessionScopeTests: XCTestCase { // Given let currentTime = Date() let scope: RUMSessionScope = .mockWith( + isInitialSession: .mockRandom(), // no matter if its initial session or not + parent: parent, samplingRate: 100, startTime: currentTime, backgroundEventTrackingEnabled: true @@ -92,7 +90,7 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertTrue(scope.viewScopes.isEmpty) // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true) + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true, canStartApplicationLaunchView: false) XCTAssertTrue(scope.process(command: command)) // Then @@ -105,8 +103,8 @@ class RUMSessionScopeTests: XCTestCase { func testGivenNoActiveViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { // Given let currentTime = Date() - let parent: RUMApplicationScope = .mockAny() let scope: RUMSessionScope = .mockWith( + isInitialSession: .mockRandom(), // no matter if its initial session or not parent: parent, samplingRate: 100, startTime: currentTime, @@ -120,20 +118,42 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertFalse(scope.viewScopes[0].isActiveView, "... but the view is not active") // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true) + let command = RUMCommandMock(time: currentTime.addingTimeInterval(2), canStartBackgroundView: true, canStartApplicationLaunchView: false) XCTAssertTrue(scope.process(command: command)) // Then XCTAssertEqual(scope.viewScopes.count, 2, "It should start background view scope") - XCTAssertEqual(scope.viewScopes[1].viewStartTime, currentTime) + XCTAssertEqual(scope.viewScopes[1].viewStartTime, currentTime.addingTimeInterval(2)) XCTAssertEqual(scope.viewScopes[1].viewName, RUMSessionScope.Constants.backgroundViewName) XCTAssertEqual(scope.viewScopes[1].viewPath, RUMSessionScope.Constants.backgroundViewURL) } - func testGivenNoViewScopeAndBackgroundEventsTrackingDisabled_whenCommandCanStartBackgroundView_itDoesNotCreateBackgroundScope() { + func testGivenNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { // Given let currentTime = Date() let scope: RUMSessionScope = .mockWith( + isInitialSession: .mockRandom(), // no matter if its initial session or not + parent: parent, + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: true + ) + XCTAssertTrue(scope.viewScopes.isEmpty) + + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false, canStartApplicationLaunchView: false) + XCTAssertTrue(scope.process(command: command)) + + // Then + XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") + } + + func testGivenNoViewScopeAndBackgroundEventsTrackingDisabled_whenReceivingAnyCommand_itNeverCreatesBackgroundScope() { + // Given + let currentTime = Date() + let scope: RUMSessionScope = .mockWith( + isInitialSession: .mockRandom(), // no matter if its initial session or not + parent: parent, samplingRate: 100, startTime: currentTime, backgroundEventTrackingEnabled: false @@ -141,43 +161,74 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertTrue(scope.viewScopes.isEmpty) // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true) + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: false) XCTAssertTrue(scope.process(command: command)) // Then - XCTAssertTrue(scope.viewScopes.isEmpty, "It shoul not start any view scope") + XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") } - func testGivenNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { + // MARK: - Application Launch Events Tracking + + func testGivenInitialSessionWithNoViewTrackedBefore_whenCommandCanStartApplicationLaunchView_itCreatesAppLaunchScope() { // Given let currentTime = Date() let scope: RUMSessionScope = .mockWith( + isInitialSession: true, + parent: parent, samplingRate: 100, startTime: currentTime, - backgroundEventTrackingEnabled: true + backgroundEventTrackingEnabled: .mockRandom() // no matter of BET state + ) + XCTAssertTrue(scope.viewScopes.isEmpty) + + // When + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false, canStartApplicationLaunchView: true) + XCTAssertTrue(scope.process(command: command)) + + // Then + XCTAssertEqual(scope.viewScopes.count, 1, "It should start application launch view scope") + XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMSessionScope.Constants.applicationLaunchViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMSessionScope.Constants.applicationLaunchViewURL) + } + + func testGivenNotInitialSessionWithNoViewTrackedBefore_whenCommandCanStartApplicationLaunchView_itDoesNotCreateAppLaunchScope() { + // Given + let currentTime = Date() + let scope: RUMSessionScope = .mockWith( + isInitialSession: false, + parent: parent, + samplingRate: 100, + startTime: currentTime, + backgroundEventTrackingEnabled: .mockRandom() // no matter of BET state ) XCTAssertTrue(scope.viewScopes.isEmpty) // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false) + let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false, canStartApplicationLaunchView: true) XCTAssertTrue(scope.process(command: command)) // Then XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") } - func testGivenNoViewScopeAndBackgroundEventsTrackingDisabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { + func testGivenAnySessionWithSomeViewsTrackedBefore_whenCommandCanStartApplicationLaunchView_itDoesNotCreateAppLaunchScope() { // Given let currentTime = Date() let scope: RUMSessionScope = .mockWith( + isInitialSession: .mockRandom(), // any session, no matter if initial or not + parent: parent, samplingRate: 100, startTime: currentTime, - backgroundEventTrackingEnabled: false + backgroundEventTrackingEnabled: .mockRandom() // no matter of BET state ) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: "view")) + _ = scope.process(command: RUMStopViewCommand.mockWith(time: currentTime.addingTimeInterval(1), identity: "view")) XCTAssertTrue(scope.viewScopes.isEmpty) // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false) + let command = RUMCommandMock(time: currentTime.addingTimeInterval(2), canStartBackgroundView: false, canStartApplicationLaunchView: true) XCTAssertTrue(scope.process(command: command)) // Then @@ -187,8 +238,6 @@ class RUMSessionScopeTests: XCTestCase { // MARK: - Sampling func testWhenSessionIsSampled_itDoesNotCreateViewScopes() { - let parent = RUMContextProviderMock() - let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 0, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) XCTAssertTrue( @@ -202,8 +251,6 @@ class RUMSessionScopeTests: XCTestCase { func testWhenNoActiveViewScopes_itLogsWarning() { // Given - let parent = RUMContextProviderMock() - let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 100, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) From 36d5e6f0183e008ad429d421cda80719dd141952 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 30 Nov 2021 16:26:15 +0100 Subject: [PATCH 4/6] RUMM-1765 Start RUM session with the first tracked event this is to capture "app launch" events before starting first view. --- .../Datadog/RUM/RUMMonitor/RUMCommand.swift | 5 -- .../Scopes/RUMApplicationScope.swift | 20 ++---- .../RUMMonitor/Scopes/RUMSessionScope.swift | 6 ++ .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 24 ++++--- .../Datadog/Mocks/RUMFeatureMocks.swift | 9 ++- .../Scopes/RUMApplicationScopeTests.swift | 9 --- .../RUMMonitor/Scopes/RUMViewScopeTests.swift | 65 ++++++++++++------- 7 files changed, 75 insertions(+), 63 deletions(-) diff --git a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift index 97c9aa95cf..e5f0047960 100644 --- a/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift +++ b/Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift @@ -36,11 +36,6 @@ internal struct RUMStartViewCommand: RUMCommand { /// The path of this View, rendered in RUM Explorer as `VIEW URL`. let path: String - /// Used to indicate if this command starts the very first View in the app. - /// * default `false` means _it's not yet known_, - /// * it can be set to `true` by the `RUMApplicationScope` which tracks this state. - var isInitialView = false - init( time: Date, identity: RUMViewIdentifiable, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 190ac1cd71..5cc0610a86 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -29,7 +29,7 @@ internal struct RUMScopeDependencies { internal class RUMApplicationScope: RUMScope, RUMContextProvider { // MARK: - Child Scopes - /// Session scope. It gets created with the first `.startView` event. + /// Session scope. It gets created with the first event. /// Might be re-created later according to session duration constraints. private(set) var sessionScope: RUMSessionScope? @@ -69,19 +69,16 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { // MARK: - RUMScope func process(command: RUMCommand) -> Bool { + if sessionScope == nil { + startInitialSession(on: command) + } + if let currentSession = sessionScope { sessionScope = manage(childScope: sessionScope, byPropagatingCommand: command) if sessionScope == nil { // if session expired refresh(expiredSession: currentSession, on: command) } - } else { - switch command { - case let command as RUMStartViewCommand: - startInitialSession(on: command) - default: - break - } } return true @@ -96,10 +93,7 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { _ = refreshedSession.process(command: command) } - private func startInitialSession(on command: RUMStartViewCommand) { - var startInitialViewCommand = command - startInitialViewCommand.isInitialView = true - + private func startInitialSession(on command: RUMCommand) { let initialSession = RUMSessionScope( isInitialSession: true, parent: self, @@ -108,10 +102,8 @@ internal class RUMApplicationScope: RUMScope, RUMContextProvider { startTime: command.time, backgroundEventTrackingEnabled: backgroundEventTrackingEnabled ) - sessionScope = initialSession sessionScopeDidUpdate(initialSession) - _ = initialSession.process(command: startInitialViewCommand) } private func sessionScopeDidUpdate(_ sessionScope: RUMSessionScope) { diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index acfdd7a572..3318e54442 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -94,6 +94,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { return nil // if the underlying identifiable (`UIVIewController`) no longer exists, skip transferring its scope } return RUMViewScope( + isInitialView: false, parent: self, dependencies: dependencies, identity: expiredViewIdentifiable, @@ -159,8 +160,10 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // MARK: - RUMCommands Processing private func startView(on command: RUMStartViewCommand) { + let isStartingInitialView = isInitialSession && !hasTrackedAnyView viewScopes.append( RUMViewScope( + isInitialView: isStartingInitialView, parent: self, dependencies: dependencies, identity: command.identity, @@ -176,6 +179,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { private func startApplicationLaunchView(on command: RUMCommand) { viewScopes.append( RUMViewScope( + isInitialView: true, parent: self, dependencies: dependencies, identity: Constants.applicationLaunchViewURL, @@ -189,8 +193,10 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { } private func startBackgroundView(on command: RUMCommand) { + let isStartingInitialView = isInitialSession && !hasTrackedAnyView viewScopes.append( RUMViewScope( + isInitialView: isStartingInitialView, parent: self, dependencies: dependencies, identity: Constants.backgroundViewURL, diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index aca25573d7..b2476d3b18 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -23,6 +23,8 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { private unowned let parent: RUMContextProvider private let dependencies: RUMScopeDependencies + /// If this is the very first view created in the current app process. + private let isInitialView: Bool /// The value holding stable identity of this RUM View. let identity: RUMViewIdentity @@ -74,6 +76,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { private let vitalInfoSampler: VitalInfoSampler init( + isInitialView: Bool, parent: RUMContextProvider, dependencies: RUMScopeDependencies, identity: RUMViewIdentifiable, @@ -85,6 +88,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { ) { self.parent = parent self.dependencies = dependencies + self.isInitialView = isInitialView self.identity = identity.asRUMViewIdentity() self.attributes = attributes self.customTimings = customTimings @@ -122,6 +126,17 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { // Propagate to User Action scope userActionScope = manage(childScope: userActionScope, byPropagatingCommand: command) + // Send "application start" action if this is the very first view tracked in the app + let hasSentNoViewUpdatesYet = version == 0 + if isInitialView && hasSentNoViewUpdatesYet { + actionsCount += 1 + if !sendApplicationStartAction() { + actionsCount -= 1 + } else { + needsViewUpdate = true + } + } + // Apply side effects switch command { // View commands @@ -132,13 +147,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { isActiveView = false } didReceiveStartCommand = true - if command.isInitialView { - actionsCount += 1 - if !sendApplicationStartAction(on: command) { - actionsCount -= 1 - break - } - } needsViewUpdate = true case let command as RUMStartViewCommand where !identity.equals(command.identity): isActiveView = false @@ -299,7 +307,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { // MARK: - Sending RUM Events - private func sendApplicationStartAction(on command: RUMCommand) -> Bool { + private func sendApplicationStartAction() -> Bool { let eventData = RUMActionEvent( dd: .init( session: .init(plan: .plan1) diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 4a939a7456..1997bd4c0f 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -167,18 +167,15 @@ extension RUMStartViewCommand { attributes: [AttributeKey: AttributeValue] = [:], identity: RUMViewIdentifiable = mockView, name: String = .mockAny(), - path: String? = nil, - isInitialView: Bool = false + path: String? = nil ) -> RUMStartViewCommand { - var command = RUMStartViewCommand( + return RUMStartViewCommand( time: time, identity: identity, name: name, path: path, attributes: attributes ) - command.isInitialView = isInitialView - return command } } @@ -535,6 +532,7 @@ extension RUMViewScope { } static func mockWith( + isInitialView: Bool = false, parent: RUMContextProvider = RUMContextProviderMock(), dependencies: RUMScopeDependencies = .mockAny(), identity: RUMViewIdentifiable = mockView, @@ -545,6 +543,7 @@ extension RUMViewScope { startTime: Date = .mockAny() ) -> RUMViewScope { return RUMViewScope( + isInitialView: isInitialView, parent: parent, dependencies: dependencies, identity: identity, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift index 523252f237..a95fa87a13 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift @@ -95,15 +95,6 @@ class RUMApplicationScopeTests: XCTestCase { XCTAssertFalse(nextSession.isInitialSession, "Any next session in the application must be marked as 'not initial'") } - func testUntilSessionIsStarted_itIgnoresOtherCommands() { - let scope = RUMApplicationScope(rumApplicationID: .mockAny(), dependencies: .mockAny(), samplingRate: 100, backgroundEventTrackingEnabled: .mockAny()) - - XCTAssertTrue(scope.process(command: RUMStopViewCommand.mockAny())) - XCTAssertTrue(scope.process(command: RUMAddUserActionCommand.mockAny())) - XCTAssertTrue(scope.process(command: RUMStopResourceCommand.mockAny())) - XCTAssertNil(scope.sessionScope) - } - // MARK: - RUM Session Sampling func testWhenSamplingRateIs100_allEventsAreSent() { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index a4b406f756..e6fb5cadb7 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -17,6 +17,7 @@ class RUMViewScopeTests: XCTestCase { let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") let sessionScope: RUMSessionScope = .mockWith(parent: applicationScope) let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: sessionScope, dependencies: .mockAny(), identity: mockView, @@ -39,6 +40,7 @@ class RUMViewScopeTests: XCTestCase { let applicationScope: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") let sessionScope: RUMSessionScope = .mockWith(parent: applicationScope) let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: sessionScope, dependencies: .mockAny(), identity: mockView, @@ -59,9 +61,10 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(scope.context.activeUserActionID, try XCTUnwrap(scope.userActionScope?.actionUUID)) } - func testWhenInitialViewIsStarted_itSendsApplicationStartAction() throws { + func testWhenInitialViewReceivesAnyCommand_itSendsApplicationStartAction() throws { let currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: true, parent: parent, dependencies: .mockWith( launchTimeProvider: LaunchTimeProviderMock(launchTime: 2), // 2 seconds @@ -75,11 +78,7 @@ class RUMViewScopeTests: XCTestCase { startTime: currentTime ) - XCTAssertTrue( - scope.process( - command: RUMStartViewCommand.mockWith(time: currentTime, attributes: ["foo": "bar"], identity: mockView, isInitialView: true) - ) - ) + _ = scope.process(command: RUMCommandMock(time: currentTime)) let event = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).first) XCTAssertEqual(event.model.date, Date.mockDecember15th2019At10AMUTC().timeIntervalSince1970.toInt64Milliseconds) @@ -95,9 +94,10 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") } - func testWhenInitialViewIsStarted_itSendsViewUpdateEvent() throws { + func testWhenInitialViewReceivesAnyCommand_itSendsViewUpdateEvent() throws { let currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: true, parent: parent, dependencies: dependencies, identity: mockView, @@ -108,11 +108,7 @@ class RUMViewScopeTests: XCTestCase { startTime: currentTime ) - XCTAssertTrue( - scope.process( - command: RUMStartViewCommand.mockWith(time: currentTime, attributes: ["foo": "bar"], identity: mockView, isInitialView: true) - ) - ) + _ = scope.process(command: RUMCommandMock(time: currentTime)) let event = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).first) XCTAssertEqual(event.model.date, Date.mockDecember15th2019At10AMUTC().timeIntervalSince1970.toInt64Milliseconds) @@ -129,13 +125,14 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.view.error.count, 0) XCTAssertEqual(event.model.view.resource.count, 0) XCTAssertEqual(event.model.dd.documentVersion, 1) - XCTAssertEqual(event.model.context?.contextInfo as? [String: String], ["foo": "bar"]) XCTAssertEqual(event.model.dd.session?.plan, .plan1, "All RUM events should use RUM Lite plan") } func testWhenViewIsStarted_itSendsViewUpdateEvent() throws { let currentTime: Date = .mockDecember15th2019At10AMUTC() + let isInitialView: Bool = .mockRandom() let scope = RUMViewScope( + isInitialView: isInitialView, parent: parent, dependencies: dependencies, identity: mockView, @@ -163,7 +160,7 @@ class RUMViewScopeTests: XCTestCase { let viewIsActive = try XCTUnwrap(event.model.view.isActive) XCTAssertTrue(viewIsActive) XCTAssertEqual(event.model.view.timeSpent, 0) - XCTAssertEqual(event.model.view.action.count, 0) + XCTAssertEqual(event.model.view.action.count, isInitialView ? 1 : 0, "It must track application start action only if this is an initial view") XCTAssertEqual(event.model.view.error.count, 0) XCTAssertEqual(event.model.view.resource.count, 0) XCTAssertEqual(event.model.dd.documentVersion, 1) @@ -172,7 +169,9 @@ class RUMViewScopeTests: XCTestCase { func testWhenViewIsStopped_itSendsViewUpdateEvent_andEndsTheScope() throws { var currentTime: Date = .mockDecember15th2019At10AMUTC() + let isInitialView: Bool = .mockRandom() let scope = RUMViewScope( + isInitialView: isInitialView, parent: parent, dependencies: dependencies, identity: mockView, @@ -213,7 +212,7 @@ class RUMViewScopeTests: XCTestCase { let viewIsActive = try XCTUnwrap(event.model.view.isActive) XCTAssertFalse(viewIsActive) XCTAssertEqual(event.model.view.timeSpent, TimeInterval(2).toInt64Nanoseconds) - XCTAssertEqual(event.model.view.action.count, 0) + XCTAssertEqual(event.model.view.action.count, isInitialView ? 1 : 0, "It must track application start action only if this is an initial view") XCTAssertEqual(event.model.view.error.count, 0) XCTAssertEqual(event.model.view.resource.count, 0) XCTAssertEqual(event.model.dd.documentVersion, 2) @@ -225,6 +224,7 @@ class RUMViewScopeTests: XCTestCase { let view2 = createMockView(viewControllerClassName: "SecondViewController") var currentTime = Date() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: view1, @@ -260,6 +260,7 @@ class RUMViewScopeTests: XCTestCase { func testWhenTheViewIsStartedAnotherTime_itEndsTheScope() throws { var currentTime = Date() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -295,6 +296,7 @@ class RUMViewScopeTests: XCTestCase { func testGivenMultipleViewScopes_whenSendingViewEvent_eachScopeUsesUniqueViewID() throws { func createScope(uri: String, name: String) -> RUMViewScope { RUMViewScope( + isInitialView: false, parent: parent, dependencies: dependencies, identity: mockView, @@ -331,6 +333,7 @@ class RUMViewScopeTests: XCTestCase { func testItManagesResourceScopesLifecycle() throws { let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -380,6 +383,7 @@ class RUMViewScopeTests: XCTestCase { func testGivenViewWithPendingResources_whenItGetsStopped_itDoesNotFinishUntilResourcesComplete() throws { let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -426,6 +430,7 @@ class RUMViewScopeTests: XCTestCase { func testItManagesContinuousUserActionScopeLifecycle() throws { let scope = RUMViewScope( + isInitialView: false, parent: parent, dependencies: dependencies, identity: mockView, @@ -486,6 +491,7 @@ class RUMViewScopeTests: XCTestCase { func testItManagesDiscreteUserActionScopeLifecycle() throws { var currentTime = Date() let scope = RUMViewScope( + isInitialView: false, parent: parent, dependencies: dependencies, identity: mockView, @@ -547,6 +553,7 @@ class RUMViewScopeTests: XCTestCase { func testWhenViewErrorIsAdded_itSendsErrorEventAndViewUpdateEvent() throws { var currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -559,7 +566,7 @@ class RUMViewScopeTests: XCTestCase { XCTAssertTrue( scope.process( - command: RUMStartViewCommand.mockWith(time: currentTime, attributes: ["foo": "bar"], identity: mockView, isInitialView: true) + command: RUMStartViewCommand.mockWith(time: currentTime, attributes: ["foo": "bar"], identity: mockView) ) ) @@ -629,6 +636,7 @@ class RUMViewScopeTests: XCTestCase { func testWhenResourceIsFinishedWithError_itSendsViewUpdateEvent() throws { let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -641,7 +649,7 @@ class RUMViewScopeTests: XCTestCase { XCTAssertTrue( scope.process( - command: RUMStartViewCommand.mockWith(attributes: ["foo": "bar"], identity: mockView, isInitialView: true) + command: RUMStartViewCommand.mockWith(attributes: ["foo": "bar"], identity: mockView) ) ) @@ -668,6 +676,7 @@ class RUMViewScopeTests: XCTestCase { let startViewDate: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -680,7 +689,7 @@ class RUMViewScopeTests: XCTestCase { XCTAssertTrue( scope.process( - command: RUMStartViewCommand.mockWith(time: startViewDate, attributes: ["foo": "bar"], identity: mockView, isInitialView: true) + command: RUMStartViewCommand.mockWith(time: startViewDate, attributes: ["foo": "bar"], identity: mockView) ) ) @@ -718,6 +727,7 @@ class RUMViewScopeTests: XCTestCase { func testGivenActiveView_whenCustomTimingIsRegistered_itSendsViewUpdateEvent() throws { var currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -770,6 +780,7 @@ class RUMViewScopeTests: XCTestCase { func testGivenInactiveView_whenCustomTimingIsRegistered_itDoesNotSendViewUpdateEvent() throws { var currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -804,6 +815,7 @@ class RUMViewScopeTests: XCTestCase { func testGivenActiveView_whenCustomTimingIsRegistered_itSanitizesCustomTiming() throws { var currentTime: Date = .mockDecember15th2019At10AMUTC() let scope = RUMViewScope( + isInitialView: .mockRandom(), parent: parent, dependencies: dependencies, identity: mockView, @@ -866,6 +878,7 @@ class RUMViewScopeTests: XCTestCase { // Given let scope = RUMViewScope( + isInitialView: false, parent: parent, dependencies: dependencies.replacing(dateCorrector: dateCorrectorMock), identity: mockView, @@ -955,6 +968,7 @@ class RUMViewScopeTests: XCTestCase { let dependencies: RUMScopeDependencies = .mockWith(eventBuilder: eventBuilder, eventOutput: output) let scope = RUMViewScope( + isInitialView: true, parent: parent, dependencies: dependencies, identity: mockView, @@ -965,7 +979,7 @@ class RUMViewScopeTests: XCTestCase { startTime: Date() ) XCTAssertTrue( - scope.process(command: RUMStartViewCommand.mockWith(identity: mockView, isInitialView: true)) + scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) ) XCTAssertTrue( @@ -1024,7 +1038,7 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.model.dd.documentVersion, 3, "After starting the application, stopping the view, starting/stopping one resource out of 2, discarding a user action and an error, the View scope should have sent 3 View events.") } - func testGivenViewScopeWithDroppingEventsMapper_whenProcessingApplicationStartAction_thenNoEventIsSent() throws { + func testGivenViewScopeWithDroppingEventsMapper_whenProcessingApplicationStartAction_thenCountIsAdjusted() throws { let eventBuilder = RUMEventBuilder( eventsMapper: .mockWith( actionEventMapper: { event in @@ -1034,7 +1048,9 @@ class RUMViewScopeTests: XCTestCase { ) let dependencies: RUMScopeDependencies = .mockWith(eventBuilder: eventBuilder, eventOutput: output) + // Given let scope = RUMViewScope( + isInitialView: true, parent: parent, dependencies: dependencies, identity: mockView, @@ -1044,10 +1060,15 @@ class RUMViewScopeTests: XCTestCase { customTimings: [:], startTime: Date() ) + + // When XCTAssertTrue( - scope.process(command: RUMStartViewCommand.mockWith(identity: mockView, isInitialView: true)) + scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) ) - XCTAssertNil(try output.recordedEvents(ofType: RUMEvent.self).last) + // Then + let event = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last) + XCTAssertEqual(event.model.view.action.count, 0, "All actions, including ApplicationStart action should be dropped") + XCTAssertEqual(event.model.dd.documentVersion, 1, "It should record only one view update") } } From cf70d8591e03aa44095faf2fea589bfb376066bf Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 1 Dec 2021 11:56:51 +0100 Subject: [PATCH 5/6] RUMM-1765 Improve RUM command mocks --- .../Datadog/Mocks/RUMDataModelMocks.swift | 8 +- .../Datadog/Mocks/RUMFeatureMocks.swift | 224 +++++++++++++++++- .../Scopes/RUMApplicationScopeTests.swift | 4 +- 3 files changed, 223 insertions(+), 13 deletions(-) diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift index 74daf07722..2fbbe91e01 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMDataModelMocks.swift @@ -201,6 +201,12 @@ extension RUMActionEvent: RandomMockable { } } +extension RUMErrorEvent.Error.SourceType: RandomMockable { + static func mockRandom() -> RUMErrorEvent.Error.SourceType { + return [.android, .browser, .ios, .reactNative].randomElement()! + } +} + extension RUMErrorEvent: RandomMockable { static func mockRandom() -> RUMErrorEvent { return RUMErrorEvent( @@ -229,7 +235,7 @@ extension RUMErrorEvent: RandomMockable { url: .mockRandom() ), source: [.source, .network, .custom].randomElement()!, - sourceType: [.android, .browser, .ios, .reactNative].randomElement()!, + sourceType: .mockRandom(), stack: .mockRandom(), type: .mockRandom() ), diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index 1997bd4c0f..70cb013cde 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -159,9 +159,38 @@ struct RUMCommandMock: RUMCommand { var canStartApplicationLaunchView = false } -extension RUMStartViewCommand { +/// Creates random `RUMCommand` from available ones. +func mockRandomRUMCommand(where predicate: (RUMCommand) -> Bool = { _ in true }) -> RUMCommand { + let allCommands: [RUMCommand] = [ + RUMStartViewCommand.mockRandom(), + RUMStopViewCommand.mockRandom(), + RUMAddCurrentViewErrorCommand.mockRandom(), + RUMAddViewTimingCommand.mockRandom(), + RUMStartResourceCommand.mockRandom(), + RUMAddResourceMetricsCommand.mockRandom(), + RUMStopResourceCommand.mockRandom(), + RUMStopResourceWithErrorCommand.mockRandom(), + RUMStartUserActionCommand.mockRandom(), + RUMStopUserActionCommand.mockRandom(), + RUMAddUserActionCommand.mockRandom(), + RUMAddLongTaskCommand.mockRandom(), + ] + return allCommands.filter(predicate).randomElement()! +} + +extension RUMStartViewCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStartViewCommand { mockWith() } + static func mockRandom() -> RUMStartViewCommand { + return .mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + identity: String.mockRandom(), + name: .mockRandom(), + path: .mockRandom() + ) + } + static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], @@ -179,9 +208,17 @@ extension RUMStartViewCommand { } } -extension RUMStopViewCommand { +extension RUMStopViewCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStopViewCommand { mockWith() } + static func mockRandom() -> RUMStopViewCommand { + return .mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + identity: String.mockRandom() + ) + } + static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], @@ -193,7 +230,29 @@ extension RUMStopViewCommand { } } -extension RUMAddCurrentViewErrorCommand { +extension RUMAddCurrentViewErrorCommand: AnyMockable, RandomMockable { + static func mockAny() -> RUMAddCurrentViewErrorCommand { .mockWithErrorObject() } + + static func mockRandom() -> RUMAddCurrentViewErrorCommand { + if Bool.random() { + return .mockWithErrorObject( + time: .mockRandomInThePast(), + error: ErrorMock(.mockRandom()), + source: .mockRandom(), + attributes: mockRandomAttributes() + ) + } else { + return .mockWithErrorMessage( + time: .mockRandomInThePast(), + message: .mockRandom(), + type: .mockRandom(), + source: .mockRandom(), + stack: .mockRandom(), + attributes: mockRandomAttributes() + ) + } + } + static func mockWithErrorObject( time: Date = Date(), error: Error = ErrorMock(), @@ -219,7 +278,17 @@ extension RUMAddCurrentViewErrorCommand { } } -extension RUMAddViewTimingCommand { +extension RUMAddViewTimingCommand: AnyMockable, RandomMockable { + static func mockAny() -> RUMAddViewTimingCommand { .mockWith() } + + static func mockRandom() -> RUMAddViewTimingCommand { + return .mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + timingName: .mockRandom() + ) + } + static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], @@ -231,9 +300,22 @@ extension RUMAddViewTimingCommand { } } -extension RUMStartResourceCommand { +extension RUMStartResourceCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStartResourceCommand { mockWith() } + static func mockRandom() -> RUMStartResourceCommand { + return .mockWith( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + url: .mockRandom(), + httpMethod: .mockRandom(), + kind: .mockAny(), + isFirstPartyRequest: .mockRandom(), + spanContext: .init(traceID: .mockRandom(), spanID: .mockRandom()) + ) + } + static func mockWith( resourceKey: String = .mockAny(), time: Date = Date(), @@ -257,9 +339,47 @@ extension RUMStartResourceCommand { } } -extension RUMStopResourceCommand { +extension RUMAddResourceMetricsCommand: AnyMockable, RandomMockable { + static func mockAny() -> RUMAddResourceMetricsCommand { mockWith() } + + static func mockRandom() -> RUMAddResourceMetricsCommand { + return mockWith( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + metrics: .mockAny() + ) + } + + static func mockWith( + resourceKey: String = .mockAny(), + time: Date = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:], + metrics: ResourceMetrics = .mockAny() + ) -> RUMAddResourceMetricsCommand { + return RUMAddResourceMetricsCommand( + resourceKey: resourceKey, + time: time, + attributes: attributes, + metrics: metrics + ) + } +} + +extension RUMStopResourceCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStopResourceCommand { mockWith() } + static func mockRandom() -> RUMStopResourceCommand { + return mockWith( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + kind: [.native, .image, .font, .other].randomElement()!, + httpStatusCode: .mockRandom(), + size: .mockRandom() + ) + } + static func mockWith( resourceKey: String = .mockAny(), time: Date = Date(), @@ -274,7 +394,32 @@ extension RUMStopResourceCommand { } } -extension RUMStopResourceWithErrorCommand { +extension RUMStopResourceWithErrorCommand: AnyMockable, RandomMockable { + static func mockAny() -> RUMStopResourceWithErrorCommand { mockWithErrorMessage() } + + static func mockRandom() -> RUMStopResourceWithErrorCommand { + if Bool.random() { + return mockWithErrorObject( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + error: ErrorMock(.mockRandom()), + source: .mockRandom(), + httpStatusCode: .mockRandom(), + attributes: mockRandomAttributes() + ) + } else { + return mockWithErrorMessage( + resourceKey: .mockRandom(), + time: .mockRandomInThePast(), + message: .mockRandom(), + type: .mockRandom(), + source: .mockRandom(), + httpStatusCode: .mockRandom(), + attributes: mockRandomAttributes() + ) + } + } + static func mockWithErrorObject( resourceKey: String = .mockAny(), time: Date = Date(), @@ -303,9 +448,18 @@ extension RUMStopResourceWithErrorCommand { } } -extension RUMStartUserActionCommand { +extension RUMStartUserActionCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStartUserActionCommand { mockWith() } + static func mockRandom() -> RUMStartUserActionCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + actionType: [.swipe, .scroll, .custom].randomElement()!, + name: .mockRandom() + ) + } + static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], @@ -318,9 +472,18 @@ extension RUMStartUserActionCommand { } } -extension RUMStopUserActionCommand { +extension RUMStopUserActionCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMStopUserActionCommand { mockWith() } + static func mockRandom() -> RUMStopUserActionCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + actionType: [.swipe, .scroll, .custom].randomElement()!, + name: .mockRandom() + ) + } + static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], @@ -333,9 +496,18 @@ extension RUMStopUserActionCommand { } } -extension RUMAddUserActionCommand { +extension RUMAddUserActionCommand: AnyMockable, RandomMockable { static func mockAny() -> RUMAddUserActionCommand { mockWith() } + static func mockRandom() -> RUMAddUserActionCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + actionType: [.tap, .custom].randomElement()!, + name: .mockRandom() + ) + } + static func mockWith( time: Date = Date(), attributes: [AttributeKey: AttributeValue] = [:], @@ -348,6 +520,38 @@ extension RUMAddUserActionCommand { } } +extension RUMAddLongTaskCommand: AnyMockable, RandomMockable { + static func mockAny() -> RUMAddLongTaskCommand { mockWith() } + + static func mockRandom() -> RUMAddLongTaskCommand { + return mockWith( + time: .mockRandomInThePast(), + attributes: mockRandomAttributes(), + duration: .mockRandom(min: 0.01, max: 1) + ) + } + + static func mockWith( + time: Date = .mockAny(), + attributes: [AttributeKey: AttributeValue] = [:], + duration: TimeInterval = 0.01 + ) -> RUMAddLongTaskCommand { + return RUMAddLongTaskCommand( + time: time, + attributes: attributes, + duration: duration + ) + } +} + +// MARK: - RUMCommand Property Mocks + +extension RUMInternalErrorSource: RandomMockable { + static func mockRandom() -> RUMInternalErrorSource { + return [.custom, .source, .network, .webview, .logger, .console].randomElement()! + } +} + // MARK: - RUMContext Mocks extension RUMUUID { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift index a95fa87a13..e580766d2f 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScopeTests.swift @@ -23,7 +23,7 @@ class RUMApplicationScopeTests: XCTestCase { XCTAssertNil(scope.context.activeUserActionID) } - func testWhenFirstViewIsStarted_itStartsNewSession() throws { + func testWhenFirstEventIsReceived_itStartsNewSession() throws { let expectation = self.expectation(description: "onSessionStart is called") let onSessionStart: RUMSessionListener = { sessionId, isDiscarded in XCTAssertTrue(sessionId.matches(regex: .uuidRegex)) @@ -41,7 +41,7 @@ class RUMApplicationScopeTests: XCTestCase { ) XCTAssertNil(scope.sessionScope) - XCTAssertTrue(scope.process(command: RUMStartViewCommand.mockAny())) + XCTAssertTrue(scope.process(command: mockRandomRUMCommand())) waitForExpectations(timeout: 0.5) let sessionScope = try XCTUnwrap(scope.sessionScope) From 51968bf448427e5e1de233aec7092a14f305ab69 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 1 Dec 2021 13:57:45 +0100 Subject: [PATCH 6/6] RUMM-1765 Test app launch events tracking in `RUMMonitorTests` --- .../Datadog/RUMMonitorTests.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift index 655e9e5974..f04a4f40f6 100644 --- a/Tests/DatadogTests/Datadog/RUMMonitorTests.swift +++ b/Tests/DatadogTests/Datadog/RUMMonitorTests.swift @@ -928,6 +928,53 @@ class RUMMonitorTests: XCTestCase { XCTAssertEqual(session.viewVisits[2].name, "another view in `.granted` consent") } + // MARK: - Tracking App Launch Events + + func testWhenCollectingEventsBeforeStartingFirstView_itTracksThemWithinApplicationLaunchView() throws { + // Given + RUMFeature.instance = .mockByRecordingRUMEventMatchers( + directories: temporaryFeatureDirectories, + dependencies: .mockWith( + dateProvider: RelativeDateProvider( + startingFrom: Date(), + advancingBySeconds: 1 + ) + ) + ) + defer { RUMFeature.instance?.deinitialize() } + + let monitor = RUMMonitor.initialize() + + // When + monitor.addUserAction(type: .custom, name: "A1") + monitor.addError(message: "E1") + monitor.startResourceLoading(resourceKey: "R1", url: URL(string: "https://foo.com/R1")!) + monitor.startView(key: "FirstView") + monitor.addUserAction(type: .tap, name: "A2") + monitor.stopResourceLoading(resourceKey: "R1", statusCode: 200, kind: .native) + + // Then + let rumEventMatchers = try RUMFeature.waitAndReturnRUMEventMatchers(count: 11) + let session = try RUMSessionMatcher.groupMatchersBySessions(rumEventMatchers)[0] + + XCTAssertEqual(session.viewVisits.count, 2, "It should track 2 views") + + let appLaunchView = session.viewVisits[0] + XCTAssertEqual(appLaunchView.name, "ApplicationLaunch", "It should track 'ApplicationLaunch' view") + 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[1].action.target?.name, "A1", "'ApplicationLaunch' should track 'A1' action") + 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") + XCTAssertEqual(appLaunchView.resourceEvents[0].resource.url, "https://foo.com/R1", "'ApplicationLaunch' should track 'R1' resource") + + let userView = session.viewVisits[1] + XCTAssertEqual(userView.name, "FirstView", "It should track user view") + XCTAssertEqual(userView.actionEvents.count, 1, "User view should track 1 action") + XCTAssertEqual(userView.actionEvents[0].action.target?.name, "A2", "User view should track 'A2' action") + } + // MARK: - Data Scrubbing func testModifyingEventsBeforeTheyGetSend() throws {