diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 12b4cbc749..f8bfc15460 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -105,6 +105,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { switch command { case let command as RUMStartViewCommand: startView(on: command) + case is RUMStartResourceCommand, is RUMAddUserActionCommand, is RUMStartUserActionCommand: + handleOrphanStartCommand(command: command) default: break } @@ -132,7 +134,23 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { ) } - // MARK: - Private + // MARK: - Private + private func handleOrphanStartCommand(command: RUMCommand) { + if viewScopes.isEmpty { + 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 timedOutOrExpired(currentTime: Date) -> Bool { let timeElapsedSinceLastInteraction = currentTime.timeIntervalSince(lastInteractionTime) diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 14ca63ecfe..7aa650c180 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -7,6 +7,11 @@ import Foundation internal class RUMViewScope: RUMScope, RUMContextProvider { + struct Constants { + static let backgroundViewURL = "com/datadog/background/view" + static let backgroundViewName = "Background" + } + // MARK: - Child Scopes /// Active Resource scopes, keyed by .resourceKey. @@ -33,7 +38,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { /// The name of this View, used as the `VIEW NAME` in RUM Explorer. let viewName: String /// The start time of this View. - private let viewStartTime: Date + let viewStartTime: Date /// Date correction to server time. private let dateCorrection: DateCorrection /// Tells if this View is the active one. @@ -129,7 +134,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { case let command as RUMStopViewCommand where identity.equals(command.identity): isActiveView = false needsViewUpdate = true - case let command as RUMAddViewTimingCommand where isActiveView: customTimings[command.timingName] = command.time.timeIntervalSince(viewStartTime).toInt64Nanoseconds needsViewUpdate = true diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index d458c49d58..2ec0c622a7 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -8,6 +8,8 @@ import XCTest @testable import Datadog class RUMSessionScopeTests: XCTestCase { + // MARK: - Unit Tests + func testDefaultContext() { let parent: RUMApplicationScope = .mockWith(rumApplicationID: "rum-123") let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: .mockAny()) @@ -77,6 +79,66 @@ 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()) + + _ = scope.process(command: RUMStartResourceCommand.mockWith(resourceKey: "/resource/1", time: currentTime)) + + XCTAssertEqual(scope.viewScopes.count,1) + 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() + let currentTime = Date() + + let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date()) + + _ = scope.process(command: RUMStartUserActionCommand.mockWith(time: currentTime)) + + XCTAssertEqual(scope.viewScopes.count,1) + XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMViewScope.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMViewScope.Constants.backgroundViewURL) + } + + func testWhenNoViewScope_andReceivedAddUserActionCommand_itCreatesNewViewScope() { + let parent = RUMContextProviderMock() + let currentTime = Date() + + let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date()) + + _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime)) + + XCTAssertEqual(scope.viewScopes.count,1) + XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMViewScope.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMViewScope.Constants.backgroundViewURL) + } + + func testWhenActiveViewScope_andReceivingStartCommand_itDoesNotCreateNewViewScope() { + let parent = RUMContextProviderMock() + let currentTime = Date() + + let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date()) + _ = scope.process(command: generateRandomNotValidStartCommand()) + _ = scope.process(command: RUMAddUserActionCommand.mockWith(time: currentTime)) + XCTAssertEqual(scope.viewScopes.count, 1) + } + + func testWhenNoActiveViewScope_andReceivingNotValidStartCommand_itDoesNotCreateNewViewScope() { + let parent = RUMContextProviderMock() + + let scope = RUMSessionScope(parent: parent, dependencies: .mockAny(), samplingRate: 100, startTime: Date()) + _ = scope.process(command: generateRandomNotValidStartCommand()) + XCTAssertEqual(scope.viewScopes.count, 0) + } + func testWhenSessionIsSampled_itDoesNotCreateViewScopes() { let parent = RUMContextProviderMock() @@ -88,4 +150,14 @@ class RUMSessionScopeTests: XCTestCase { ) XCTAssertEqual(scope.viewScopes.count, 0) } + + // 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()! + } }