From 87f9639514636ad12d9de143a874ac8fe7cb4bb1 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Tue, 14 Dec 2021 13:32:09 +0100 Subject: [PATCH 1/2] RUMM-1765 Update `CrashContext` with RUM session info and application foreground / background state so it can be accessed in restarted session, when deciding on how to upload crash report. --- Datadog/Datadog.xcodeproj/project.pbxproj | 4 + .../Core/System/AppStateListener.swift | 13 + .../CrashContext/CrashContext.swift | 16 +- .../CrashContext/CrashContextProvider.swift | 22 +- .../CrashReporting/CrashReporter.swift | 4 +- .../CrashReportingFeature.swift | 10 +- .../RUMWithCrashContextIntegration.swift | 18 +- .../Scopes/RUMApplicationScope.swift | 3 + .../Scopes/RUMOffViewEventsHandlingRule.swift | 88 +++++++ .../RUMMonitor/Scopes/RUMSessionScope.swift | 77 +++--- .../RUM/RUMMonitor/Scopes/RUMViewScope.swift | 9 +- Sources/Datadog/RUM/UUIDs/RUMUUID.swift | 2 +- Sources/Datadog/RUMMonitor.swift | 1 + Sources/Datadog/Utils/SwiftExtensions.swift | 8 + .../CrashContextProviderTests.swift | 117 ++++++++- .../CrashContext/CrashContextTests.swift | 33 +++ .../RUMWithCrashContextIntegrationTests.swift | 13 + .../Datadog/Mocks/CoreMocks.swift | 21 +- .../Mocks/CrashReportingFeatureMocks.swift | 12 +- .../Datadog/Mocks/RUMFeatureMocks.swift | 14 ++ .../SystemFrameworks/FoundationMocks.swift | 10 + .../Scopes/RUMSessionScopeTests.swift | 224 ++++++++++++------ .../RUMMonitor/Scopes/RUMViewScopeTests.swift | 36 ++- .../Datadog/Utils/SwiftExtensionsTests.swift | 7 + 24 files changed, 622 insertions(+), 140 deletions(-) create mode 100644 Sources/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRule.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index a97044dde8..0080f902d1 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -312,6 +312,7 @@ 619E16D82577C1CB00B2516B /* DataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16D72577C1CB00B2516B /* DataProcessor.swift */; }; 619E16E92578E73E00B2516B /* DataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16E82578E73E00B2516B /* DataMigrator.swift */; }; 619E16F12578E89700B2516B /* DeleteAllDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16F02578E89700B2516B /* DeleteAllDataMigrator.swift */; }; + 61A614E8276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */; }; 61A763DC252DB2B3005A23F2 /* NSURLSessionBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */; }; 61A9238E256FCAA2009B9667 /* DateCorrectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A9238D256FCAA2009B9667 /* DateCorrectionTests.swift */; }; 61AADBDD263C7ECF008ABC6F /* EquatableInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AADBDC263C7ECF008ABC6F /* EquatableInTests.swift */; }; @@ -983,6 +984,7 @@ 619E16D72577C1CB00B2516B /* DataProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProcessor.swift; sourceTree = ""; }; 619E16E82578E73E00B2516B /* DataMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrator.swift; sourceTree = ""; }; 619E16F02578E89700B2516B /* DeleteAllDataMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllDataMigrator.swift; sourceTree = ""; }; + 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOffViewEventsHandlingRule.swift; sourceTree = ""; }; 61A763D9252DB2B3005A23F2 /* DatadogTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DatadogTests-Bridging-Header.h"; sourceTree = ""; }; 61A763DA252DB2B3005A23F2 /* NSURLSessionBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionBridge.h; sourceTree = ""; }; 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionBridge.m; sourceTree = ""; }; @@ -2797,6 +2799,7 @@ isa = PBXGroup; children = ( 61FF9A4425AC5DEA001058CC /* RUMViewIdentity.swift */, + 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */, 61C3E63D24BF1B91008053F2 /* RUMApplicationScope.swift */, 61C2C20624C098FC00C0321C /* RUMSessionScope.swift */, 61C2C21124C5951400C0321C /* RUMViewScope.swift */, @@ -4075,6 +4078,7 @@ 61133BE72423979B00786299 /* LogUtilityOutputs.swift in Sources */, 61133BDA2423979B00786299 /* RequestBuilder.swift in Sources */, 61C3E63924BF19B4008053F2 /* RUMContext.swift in Sources */, + 61A614E8276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift in Sources */, 61ED39D426C2A36B002C0F26 /* DataUploadStatus.swift in Sources */, 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */, 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, diff --git a/Sources/Datadog/Core/System/AppStateListener.swift b/Sources/Datadog/Core/System/AppStateListener.swift index 850a53f31d..8336f0fe39 100644 --- a/Sources/Datadog/Core/System/AppStateListener.swift +++ b/Sources/Datadog/Core/System/AppStateListener.swift @@ -89,9 +89,16 @@ internal struct AppStateHistory: Equatable { } } +/// An observer of `AppStateHistory` value. +internal typealias AppStateHistoryObserver = ValueObserver + /// Provides history of app foreground / background states. internal protocol AppStateListening: AnyObject { + /// Last published `AppStateHistory`. var history: AppStateHistory { get } + + /// Subscribers observer to be notified on `AppStateHistory` changes. + func subscribe(_ subscriber: Observer) where Observer.ObservedValue == AppStateHistory } internal class AppStateListener: AppStateListening { @@ -144,4 +151,10 @@ internal class AppStateListener: AppStateListening { value.changes.append(Snapshot(isActive: true, date: now)) publisher.publishAsync(value) } + + // MARK: - Managing Subscribers + + func subscribe(_ subscriber: Observer) where Observer.ObservedValue == AppStateHistory { + publisher.subscribe(subscriber) + } } diff --git a/Sources/Datadog/CrashReporting/CrashContext/CrashContext.swift b/Sources/Datadog/CrashReporting/CrashContext/CrashContext.swift index ddd037224c..81515f31f0 100644 --- a/Sources/Datadog/CrashReporting/CrashContext/CrashContext.swift +++ b/Sources/Datadog/CrashReporting/CrashContext/CrashContext.swift @@ -19,13 +19,17 @@ internal struct CrashContext: Codable { lastUserInfo: UserInfo, lastRUMViewEvent: RUMEvent?, lastNetworkConnectionInfo: NetworkConnectionInfo?, - lastCarrierInfo: CarrierInfo? + lastCarrierInfo: CarrierInfo?, + lastRUMSessionState: RUMSessionState?, + lastIsAppInForeground: Bool ) { self.codableTrackingConsent = .init(from: lastTrackingConsent) self.codableLastUserInfo = .init(from: lastUserInfo) self.codableLastRUMViewEvent = lastRUMViewEvent.flatMap { .init(from: $0) } self.codableLastNetworkConnectionInfo = lastNetworkConnectionInfo.flatMap { .init(from: $0) } self.codableLastCarrierInfo = lastCarrierInfo.flatMap { .init(from: $0) } + self.lastRUMSessionState = lastRUMSessionState + self.lastIsAppInForeground = lastIsAppInForeground } // MARK: - Codable values @@ -42,6 +46,8 @@ internal struct CrashContext: Codable { case codableLastUserInfo = "lui" case codableLastNetworkConnectionInfo = "lni" case codableLastCarrierInfo = "lci" + case lastRUMSessionState = "rst" + case lastIsAppInForeground = "aif" } // MARK: - Setters & Getters using managed types @@ -70,6 +76,14 @@ internal struct CrashContext: Codable { set { codableLastCarrierInfo = newValue.flatMap { CodableCarrierInfo(from: $0) } } get { codableLastCarrierInfo?.managedValue } } + + // MARK: - Direct Codable values + + /// State of the last RUM session in crashed app process. + var lastRUMSessionState: RUMSessionState? + + /// The last _"Is app in foreground?"_ information from crashed app process. + var lastIsAppInForeground: Bool } // MARK: - Bridging managed types to Codable representation diff --git a/Sources/Datadog/CrashReporting/CrashContext/CrashContextProvider.swift b/Sources/Datadog/CrashReporting/CrashContext/CrashContextProvider.swift index 9b435d520e..f733f2dade 100644 --- a/Sources/Datadog/CrashReporting/CrashContext/CrashContextProvider.swift +++ b/Sources/Datadog/CrashReporting/CrashContext/CrashContextProvider.swift @@ -63,6 +63,16 @@ internal class CrashContextProvider: CrashContextProviderType { self.unsafeCrashContext.lastRUMViewEvent = newValue } + /// Updates `CrashContext` with last RUM session state. + private lazy var rumSessionStateUpdater = ContextValueUpdater(queue: queue) { newValue in + self.unsafeCrashContext.lastRUMSessionState = newValue + } + + /// Updates `CrashContext` with last app foreground / background state information. + private lazy var isAppInForegroundUpdater = ContextValueUpdater(queue: queue) { newValue in + self.unsafeCrashContext.lastIsAppInForeground = newValue.currentState.isActive + } + // MARK: - Initializer init( @@ -70,7 +80,9 @@ internal class CrashContextProvider: CrashContextProviderType { userInfoProvider: UserInfoProvider, networkConnectionInfoProvider: NetworkConnectionInfoProviderType, carrierInfoProvider: CarrierInfoProviderType, - rumViewEventProvider: ValuePublisher?> + rumViewEventProvider: ValuePublisher?>, + rumSessionStateProvider: ValuePublisher, + appStateListener: AppStateListening ) { self.queue = DispatchQueue( label: "com.datadoghq.crash-context", @@ -80,9 +92,11 @@ internal class CrashContextProvider: CrashContextProviderType { self.unsafeCrashContext = CrashContext( lastTrackingConsent: consentProvider.currentValue, lastUserInfo: userInfoProvider.value, - lastRUMViewEvent: nil, + lastRUMViewEvent: rumViewEventProvider.currentValue, lastNetworkConnectionInfo: networkConnectionInfoProvider.current, - lastCarrierInfo: carrierInfoProvider.current + lastCarrierInfo: carrierInfoProvider.current, + lastRUMSessionState: rumSessionStateProvider.currentValue, + lastIsAppInForeground: appStateListener.history.currentState.isActive ) // Subscribe for context updates @@ -91,6 +105,8 @@ internal class CrashContextProvider: CrashContextProviderType { networkConnectionInfoProvider.subscribe(networkConnectionInfoUpdater) carrierInfoProvider.subscribe(carrierInfoUpdater) rumViewEventProvider.subscribe(rumViewEventUpdater) + rumSessionStateProvider.subscribe(rumSessionStateUpdater) + appStateListener.subscribe(isAppInForegroundUpdater) } // MARK: - CrashContextProviderType diff --git a/Sources/Datadog/CrashReporting/CrashReporter.swift b/Sources/Datadog/CrashReporting/CrashReporter.swift index b1ac43776b..1a5d90ade3 100644 --- a/Sources/Datadog/CrashReporting/CrashReporter.swift +++ b/Sources/Datadog/CrashReporting/CrashReporter.swift @@ -50,7 +50,9 @@ internal class CrashReporter { userInfoProvider: crashReportingFeature.userInfoProvider, networkConnectionInfoProvider: crashReportingFeature.networkConnectionInfoProvider, carrierInfoProvider: crashReportingFeature.carrierInfoProvider, - rumViewEventProvider: crashReportingFeature.rumViewEventProvider + rumViewEventProvider: crashReportingFeature.rumViewEventProvider, + rumSessionStateProvider: crashReportingFeature.rumSessionStateProvider, + appStateListener: crashReportingFeature.appStateListener ), loggingOrRUMIntegration: availableLoggingOrRUMIntegration ) diff --git a/Sources/Datadog/CrashReporting/CrashReportingFeature.swift b/Sources/Datadog/CrashReporting/CrashReportingFeature.swift index ad20c6fe23..c1c889568f 100644 --- a/Sources/Datadog/CrashReporting/CrashReportingFeature.swift +++ b/Sources/Datadog/CrashReporting/CrashReportingFeature.swift @@ -29,7 +29,13 @@ internal final class CrashReportingFeature { /// Publishes recent `CarrierInfo` value so it can be persisted in `CrashContext`. let carrierInfoProvider: CarrierInfoProviderType /// Publishes recent `RUMEvent` value so it can be persisted in `CrashContext`. + /// It will provide `nil` until first view is tracked. let rumViewEventProvider: ValuePublisher?> + /// Publishes recent RUM session state so it can be persisted in `CrashContext`. + /// It will be used to decide if and how to track crashes which happen while there was no active view. + let rumSessionStateProvider: ValuePublisher + /// Publishes changes to app "foreground" / "background" state. + let appStateListener: AppStateListening init( configuration: FeaturesConfiguration.CrashReporting, @@ -40,7 +46,9 @@ internal final class CrashReportingFeature { self.userInfoProvider = commonDependencies.userInfoProvider self.networkConnectionInfoProvider = commonDependencies.networkConnectionInfoProvider self.carrierInfoProvider = commonDependencies.carrierInfoProvider - self.rumViewEventProvider = ValuePublisher(initialValue: nil) + self.rumViewEventProvider = ValuePublisher(initialValue: nil) // `nil` by default, because there cannot be any RUM view at this ponit + self.rumSessionStateProvider = ValuePublisher(initialValue: nil) // `nil` by default, because there cannot be any RUM session at this ponit + self.appStateListener = commonDependencies.appStateListener } #if DD_SDK_COMPILED_FOR_TESTING diff --git a/Sources/Datadog/FeaturesIntegration/RUMWithCrashContextIntegration.swift b/Sources/Datadog/FeaturesIntegration/RUMWithCrashContextIntegration.swift index d76c2b7b30..9acbd1de04 100644 --- a/Sources/Datadog/FeaturesIntegration/RUMWithCrashContextIntegration.swift +++ b/Sources/Datadog/FeaturesIntegration/RUMWithCrashContextIntegration.swift @@ -6,26 +6,38 @@ import Foundation -/// Updates `CrashContext` passed to crash reporter with the last `RUMViewEvent`. +/// Updates `CrashContext` passed to crash reporter with the last RUM view and RUM session state. /// When the app restarts after crash, this event is used to create and send `RUMErrorEvent` describing the crash. /// /// This integration isolates the direct link between RUM and Crash Reporting. internal struct RUMWithCrashContextIntegration { private weak var rumViewEventProvider: ValuePublisher?>? + private weak var rumSessionStateProvider: ValuePublisher? init?() { if let crashReportingFeature = CrashReportingFeature.instance { - self.init(rumViewEventProvider: crashReportingFeature.rumViewEventProvider) + self.init( + rumViewEventProvider: crashReportingFeature.rumViewEventProvider, + rumSessionStateProvider: crashReportingFeature.rumSessionStateProvider + ) } else { return nil } } - init(rumViewEventProvider: ValuePublisher?>) { + init( + rumViewEventProvider: ValuePublisher?>, + rumSessionStateProvider: ValuePublisher + ) { self.rumViewEventProvider = rumViewEventProvider + self.rumSessionStateProvider = rumSessionStateProvider } func update(lastRUMViewEvent: RUMEvent) { rumViewEventProvider?.publishAsync(lastRUMViewEvent) } + + func update(lastRUMSessionState: RUMSessionState) { + rumSessionStateProvider?.publishAsync(lastRUMSessionState) + } } diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift index 2fa181b983..e5adeca597 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift @@ -19,6 +19,9 @@ internal struct RUMScopeDependencies { let rumUUIDGenerator: RUMUUIDGenerator /// Adjusts RUM events time (device time) to server time. let dateCorrector: DateCorrectorType + /// Integration with Crash Reporting. It updates the crash context with RUM info. + /// `nil` if Crash Reporting feature is not enabled. + let crashContextIntegration: RUMWithCrashContextIntegration? let vitalCPUReader: SamplingBasedVitalReader let vitalMemoryReader: SamplingBasedVitalReader diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRule.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRule.swift new file mode 100644 index 0000000000..b5616fd908 --- /dev/null +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRule.swift @@ -0,0 +1,88 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Lightweight representation of current RUM session state, used to compute `RUMOffViewEventsHandlingRule`. +/// It gets serialized into `CrashContext` for computing the rule upon app process restart after crash. +internal struct RUMSessionState: Equatable, Codable { + /// The session ID. Can be `.nullUUID` if the session was rejected by sampler. + let sessionUUID: UUID + /// If this is the very first session in the app process (`true`) or was re-created upon timeout (`false`). + let isInitialSession: Bool + /// If this session has ever tracked any view (used to reason about "application launch" events). + let hasTrackedAnyView: Bool +} + +/// The rule for handling RUM events which are tracked while there is no active view. +/// +/// It isolates the logic behind starting artificial views like "ApplicationLaunch" or "Background". It is used by both RUM and Crash Reporting +/// to decide on how to track off-view events and crashes. +internal enum RUMOffViewEventsHandlingRule: Equatable { + struct Constants { + /// The name of the 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 the 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 the view created when receiving an event before any view was started in the initial session. + static let applicationLaunchViewName = "ApplicationLaunch" + /// The url of the view created when receiving an event before any view was started in the initial session. + static let applicationLaunchViewURL = "com/datadog/application-launch/view" + } + + /// Start "ApplicationLaunch" view to track the event. + case handleInApplicationLaunchView + /// Start "Background" view to track the event. + case handleInBackgroundView + /// Do not start any view (drop the event). + case doNotHandle + + // MARK: - Init + + /// - Parameters: + /// - sessionState: RUM session state or `nil` if no session is started + /// - isAppInForeground: if the app is in foreground + /// - isBETEnabled: if Background Events Tracking feature is enabled in SDK configuration + init( + sessionState: RUMSessionState?, + isAppInForeground: Bool, + isBETEnabled: Bool + ) { + if let session = sessionState { + guard session.sessionUUID != .nullUUID else { + self = .doNotHandle // when session is sampled, do not track off-view events at all + return + } + + let thereWasNoViewInThisSession = !session.hasTrackedAnyView + let thereWasNoViewInThisAppProcess = session.isInitialSession && thereWasNoViewInThisSession + + if thereWasNoViewInThisAppProcess { + if isAppInForeground { + self = .handleInApplicationLaunchView + } else if isBETEnabled { + self = .handleInBackgroundView + } else { + self = .doNotHandle + } + } else { + if !isAppInForeground && isBETEnabled { + self = .handleInBackgroundView + } else { + self = .doNotHandle + } + } + } else { + if isAppInForeground { + self = .handleInApplicationLaunchView + } else if isBETEnabled { + self = .handleInBackgroundView + } else { + self = .doNotHandle + } + } + } +} diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift index 2e6e29d2d1..38bcc04b6b 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift @@ -12,14 +12,6 @@ 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" - /// 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 @@ -27,11 +19,18 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { /// Active View scopes. Scopes are added / removed when the View starts / stops displaying. private(set) var viewScopes: [RUMViewScope] = [] { didSet { - hasTrackedAnyView = hasTrackedAnyView || !viewScopes.isEmpty + if !state.hasTrackedAnyView && !viewScopes.isEmpty { + state = RUMSessionState(sessionUUID: state.sessionUUID, isInitialSession: state.isInitialSession, hasTrackedAnyView: true) + } + } + } + + /// Information about this session state, shared with `CrashContext`. + private var state: RUMSessionState { + didSet { + dependencies.crashContextIntegration?.update(lastRUMSessionState: state) } } - /// If this session has ever tracked any view. - private var hasTrackedAnyView = false // MARK: - Initialization @@ -71,6 +70,10 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { self.sessionStartTime = startTime self.lastInteractionTime = startTime self.backgroundEventTrackingEnabled = backgroundEventTrackingEnabled + self.state = RUMSessionState(sessionUUID: sessionUUID.rawValue, isInitialSession: isInitialSession, hasTrackedAnyView: false) + + // Update `CrashContext` with recent RUM session state: + dependencies.crashContextIntegration?.update(lastRUMSessionState: state) } /// Creates a new Session upon expiration of the previous one. @@ -126,31 +129,37 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { return true } - // Consider starting an active view, "ApplicationLaunch" view or "Background" view if let startViewCommand = command as? RUMStartViewCommand { + // Start view scope explicitly on receiving "start view" command startView(on: startViewCommand) - } else if isInitialSession && !hasTrackedAnyView { // if initial session with no views history - let appInForeground = dependencies.appStateListener.history.currentState.isActive - if appInForeground && command.canStartApplicationLaunchView { // when app is in foreground, start "ApplicationLaunch" view + } else if !hasActiveView { + // Otherwise, if there is no active view scope, consider starting artificial scope for handling this command + let handlingRule = RUMOffViewEventsHandlingRule( + sessionState: state, + isAppInForeground: dependencies.appStateListener.history.currentState.isActive, + isBETEnabled: backgroundEventTrackingEnabled + ) + + switch handlingRule { + case .handleInApplicationLaunchView where command.canStartApplicationLaunchView: startApplicationLaunchView(on: command) - } else if backgroundEventTrackingEnabled && command.canStartBackgroundView { // when app is in background and BET is enabled, start "Background" view + case .handleInBackgroundView where command.canStartBackgroundView: startBackgroundView(on: command) + default: + // As no view scope will handle this command, warn the user on dropping it + userLogger.warn( + """ + \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the + DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using + the RumMonitor.startView() and RumMonitor.stopView() methods. + """ + ) } - } else if backgroundEventTrackingEnabled && !hasActiveView && command.canStartBackgroundView { // if existing session with views history and BET is enabled - startBackgroundView(on: command) } // Propagate command if !viewScopes.isEmpty { viewScopes = manage(childScopes: viewScopes, byPropagatingCommand: command) - } else { - userLogger.warn( - """ - \(String(describing: command)) was detected, but no view is active. To track views automatically, try calling the - DatadogConfiguration.Builder.trackUIKitRUMViews() method. You can also track views manually using - the RumMonitor.startView() and RumMonitor.stopView() methods. - """ - ) } return true @@ -164,7 +173,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { // MARK: - RUMCommands Processing private func startView(on command: RUMStartViewCommand) { - let isStartingInitialView = isInitialSession && !hasTrackedAnyView + let isStartingInitialView = isInitialSession && !state.hasTrackedAnyView viewScopes.append( RUMViewScope( isInitialView: isStartingInitialView, @@ -186,9 +195,9 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { isInitialView: true, parent: self, dependencies: dependencies, - identity: Constants.applicationLaunchViewURL, - path: Constants.applicationLaunchViewURL, - name: Constants.applicationLaunchViewName, + identity: RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL, + path: RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL, + name: RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName, attributes: command.attributes, customTimings: [:], startTime: sessionStartTime @@ -197,15 +206,15 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider { } private func startBackgroundView(on command: RUMCommand) { - let isStartingInitialView = isInitialSession && !hasTrackedAnyView + let isStartingInitialView = isInitialSession && !state.hasTrackedAnyView viewScopes.append( RUMViewScope( isInitialView: isStartingInitialView, parent: self, dependencies: dependencies, - identity: Constants.backgroundViewURL, - path: Constants.backgroundViewURL, - name: Constants.backgroundViewName, + identity: RUMOffViewEventsHandlingRule.Constants.backgroundViewURL, + path: RUMOffViewEventsHandlingRule.Constants.backgroundViewURL, + name: RUMOffViewEventsHandlingRule.Constants.backgroundViewName, attributes: command.attributes, customTimings: [:], startTime: command.time diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index 155c9dfc5f..3eb5316c0c 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -69,10 +69,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { /// It can be toggled from inside `RUMResourceScope`/`RUMUserActionScope` callbacks, as they are called from processing `RUMCommand`s inside `process()`. private var needsViewUpdate = false - /// Integration with Crash Reporting. It updates the context of crash reporter with last `RUMViewEvent` information. - /// `nil` if Crash Reporting feature is not enabled. - private let crashContextIntegration: RUMWithCrashContextIntegration? - private let vitalInfoSampler: VitalInfoSampler init( @@ -97,7 +93,6 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { self.viewName = name self.viewStartTime = startTime self.dateCorrection = dependencies.dateCorrector.currentCorrection - self.crashContextIntegration = RUMWithCrashContextIntegration() self.vitalInfoSampler = VitalInfoSampler( cpuReader: dependencies.vitalCPUReader, @@ -415,7 +410,9 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { if let event = dependencies.eventBuilder.createRUMEvent(with: eventData) { dependencies.eventOutput.write(rumEvent: event) - crashContextIntegration?.update(lastRUMViewEvent: event) + + // Update `CrashContext` with recent RUM view: + dependencies.crashContextIntegration?.update(lastRUMViewEvent: event) } else { version -= 1 } diff --git a/Sources/Datadog/RUM/UUIDs/RUMUUID.swift b/Sources/Datadog/RUM/UUIDs/RUMUUID.swift index 002ba46cdf..3dc511c292 100644 --- a/Sources/Datadog/RUM/UUIDs/RUMUUID.swift +++ b/Sources/Datadog/RUM/UUIDs/RUMUUID.swift @@ -10,7 +10,7 @@ internal struct RUMUUID: Equatable { let rawValue: UUID /// UUID with all zeros, used to represent no-op values. - static let nullUUID = RUMUUID(rawValue: UUID(uuidString: "00000000-0000-0000-0000-000000000000") ?? UUID()) + static let nullUUID = RUMUUID(rawValue: .nullUUID) } extension Optional where Wrapped == RUMUUID { diff --git a/Sources/Datadog/RUMMonitor.swift b/Sources/Datadog/RUMMonitor.swift index f4824f8198..5c652ad396 100644 --- a/Sources/Datadog/RUMMonitor.swift +++ b/Sources/Datadog/RUMMonitor.swift @@ -193,6 +193,7 @@ public class RUMMonitor: DDRUMMonitor, RUMCommandSubscriber { ), rumUUIDGenerator: DefaultRUMUUIDGenerator(), dateCorrector: rumFeature.dateCorrector, + crashContextIntegration: RUMWithCrashContextIntegration(), vitalCPUReader: rumFeature.vitalCPUReader, vitalMemoryReader: rumFeature.vitalMemoryReader, vitalRefreshRateReader: rumFeature.vitalRefreshRateReader, diff --git a/Sources/Datadog/Utils/SwiftExtensions.swift b/Sources/Datadog/Utils/SwiftExtensions.swift index df52d6ceda..54dd684aa2 100644 --- a/Sources/Datadog/Utils/SwiftExtensions.swift +++ b/Sources/Datadog/Utils/SwiftExtensions.swift @@ -25,6 +25,14 @@ extension Double { } } +// MARK: - UUID + +extension UUID { + /// An UUID with all zeroes (`00000000-0000-0000-0000-000000000000`). + /// Used to represent "null" in types that cannot be given a proper UUID (e.g. rejected RUM session). + static let nullUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000") ?? UUID() +} + // MARK: - TimeInterval extension TimeInterval { diff --git a/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift b/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift index 65bbe66c85..4f6ef08e85 100644 --- a/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift +++ b/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextProviderTests.swift @@ -24,7 +24,9 @@ class CrashContextProviderTests: XCTestCase { userInfoProvider: .mockAny(), networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), carrierInfoProvider: CarrierInfoProviderMock.mockAny(), - rumViewEventProvider: .mockRandom() + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) let initialContext = crashContextProvider.currentCrashContext @@ -45,24 +47,30 @@ class CrashContextProviderTests: XCTestCase { // MARK: - `RUMViewEvent` Integration - func testWhenRUMWithCrashContextIntegrationIsUpdated_thenCrashContextProviderNotifiesNewContext() { + func testWhenRUMWithCrashContextIntegrationIsUpdatedWithRUMViewEvent_thenCrashContextProviderNotifiesNewContext() { let expectation = self.expectation(description: "Notify new crash context") - let randomRUMViewEvent: RUMEvent = RUMEvent(model: RUMViewEvent.mockRandom()) + let initialRUMViewEvent: RUMEvent = .mockRandom() + let randomRUMViewEvent: RUMEvent = .mockRandom() - let rumViewEventProvider = ValuePublisher?>(initialValue: randomRUMViewEvent) + let rumViewEventProvider = ValuePublisher?>(initialValue: initialRUMViewEvent) let crashContextProvider = CrashContextProvider( consentProvider: .mockAny(), userInfoProvider: .mockAny(), networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), carrierInfoProvider: CarrierInfoProviderMock.mockAny(), - rumViewEventProvider: rumViewEventProvider + rumViewEventProvider: rumViewEventProvider, + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) let initialContext = crashContextProvider.currentCrashContext var updatedContext: CrashContext? // When - let rumWithCrashContextIntegration = RUMWithCrashContextIntegration(rumViewEventProvider: rumViewEventProvider) + let rumWithCrashContextIntegration = RUMWithCrashContextIntegration( + rumViewEventProvider: rumViewEventProvider, + rumSessionStateProvider: .mockAny() + ) crashContextProvider.onCrashContextChange = { newContext in updatedContext = newContext expectation.fulfill() @@ -71,10 +79,48 @@ class CrashContextProviderTests: XCTestCase { // Then waitForExpectations(timeout: 1, handler: nil) - XCTAssertNil(initialContext.lastRUMViewEvent) + XCTAssertEqual(initialContext.lastRUMViewEvent, initialRUMViewEvent) XCTAssertEqual(updatedContext?.lastRUMViewEvent, randomRUMViewEvent) } + // MARK: - RUM Session State Integration + + func testWhenRUMWithCrashContextIntegrationIsUpdatedWithRUMSessionState_thenCrashContextProviderNotifiesNewContext() { + let expectation = self.expectation(description: "Notify new crash context") + let initialRUMSessionState: RUMSessionState = .mockRandom() + let randomRUMSessionState: RUMSessionState = .mockRandom() + + let rumSessionStateProvider = ValuePublisher(initialValue: initialRUMSessionState) + let crashContextProvider = CrashContextProvider( + consentProvider: .mockAny(), + userInfoProvider: .mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), + carrierInfoProvider: CarrierInfoProviderMock.mockAny(), + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: rumSessionStateProvider, + appStateListener: AppStateListenerMock.mockAny() + ) + + let initialContext = crashContextProvider.currentCrashContext + var updatedContext: CrashContext? + + // When + let rumWithCrashContextIntegration = RUMWithCrashContextIntegration( + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: rumSessionStateProvider + ) + crashContextProvider.onCrashContextChange = { newContext in + updatedContext = newContext + expectation.fulfill() + } + rumWithCrashContextIntegration.update(lastRUMSessionState: randomRUMSessionState) + + // Then + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(initialContext.lastRUMSessionState, initialRUMSessionState) + XCTAssertEqual(updatedContext?.lastRUMSessionState, randomRUMSessionState) + } + // MARK: - `UserInfo` Integration func testWhenUserInfoValueChangesInUserInfoProvider_thenCrashContextProviderNotifiesNewContext() { @@ -90,7 +136,9 @@ class CrashContextProviderTests: XCTestCase { userInfoProvider: userInfoProvider, networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), carrierInfoProvider: CarrierInfoProviderMock.mockAny(), - rumViewEventProvider: .mockRandom() + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) let initialContext = crashContextProvider.currentCrashContext @@ -122,7 +170,9 @@ class CrashContextProviderTests: XCTestCase { userInfoProvider: .mockAny(), networkConnectionInfoProvider: mainProvider, carrierInfoProvider: CarrierInfoProviderMock.mockAny(), - rumViewEventProvider: .mockRandom() + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) let initialContext = crashContextProvider.currentCrashContext @@ -160,7 +210,9 @@ class CrashContextProviderTests: XCTestCase { userInfoProvider: .mockAny(), networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), carrierInfoProvider: carrierInfoProvider, - rumViewEventProvider: .mockRandom() + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) let initialContext = crashContextProvider.currentCrashContext @@ -198,7 +250,9 @@ class CrashContextProviderTests: XCTestCase { userInfoProvider: .mockAny(), networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), carrierInfoProvider: carrierInfoProvider, - rumViewEventProvider: .mockRandom() + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) let initialContext = crashContextProvider.currentCrashContext @@ -224,6 +278,43 @@ class CrashContextProviderTests: XCTestCase { } } + // MARK: - `AppStateListener` Integration + + func testWhenAppStateChangeIsTrackedByAppStateListener_thenCrashContextProviderNotifiesNewContext() { + let expectation = self.expectation(description: "Notify new crash context") + + let notificationCenter = NotificationCenter() + let appStateListener = AppStateListener( + dateProvider: SystemDateProvider(), + notificationCenter: notificationCenter + ) + + let crashContextProvider = CrashContextProvider( + consentProvider: .mockAny(), + userInfoProvider: .mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), + carrierInfoProvider: CarrierInfoProviderMock.mockAny(), + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: appStateListener + ) + + let initialContext = crashContextProvider.currentCrashContext + var updatedContext: CrashContext? + + // When + crashContextProvider.onCrashContextChange = { newContext in + updatedContext = newContext + expectation.fulfill() + } + notificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) // app goes to background + + // Then + waitForExpectations(timeout: 1, handler: nil) + XCTAssertTrue(initialContext.lastIsAppInForeground, "It must track initial app state ('foreground')") + XCTAssertEqual(updatedContext?.lastIsAppInForeground, false, "It must track app state update (to 'background')") + } + // MARK: - Thread safety func testWhenContextIsWrittenAndReadFromDifferentThreads_itRunsAllOperationsSafely() { @@ -240,7 +331,9 @@ class CrashContextProviderTests: XCTestCase { userInfoProvider: userInfoProvider, networkConnectionInfoProvider: networkInfoMainProvider, carrierInfoProvider: carrierInfoMainProvider, - rumViewEventProvider: .mockRandom() + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: .mockAny(), + appStateListener: AppStateListenerMock.mockAny() ) withExtendedLifetime(provider) { diff --git a/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift b/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift index 08e3f98905..f4a2c2c0d1 100644 --- a/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift +++ b/Tests/DatadogTests/Datadog/CrashReporting/CrashContext/CrashContextTests.swift @@ -46,6 +46,24 @@ class CrashContextTests: XCTestCase { ) } + func testGivenContextWithLastRUMSessionStateSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomRUMSessionState: RUMSessionState? = Bool.random() ? .mockRandom() : nil + + // Given + var context: CrashContext = .mockRandom() + context.lastRUMSessionState = randomRUMSessionState + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + try AssertEncodedRepresentationsEqual( + value1: deserializedContext.lastRUMSessionState, + value2: randomRUMSessionState + ) + } + func testGivenContextWithUserInfoSet_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { let randomUserInfo: UserInfo = .mockRandom() @@ -97,6 +115,21 @@ class CrashContextTests: XCTestCase { XCTAssertEqual(deserializedContext.lastCarrierInfo, randomCarrierInfo) } + func testGivenContextWithIsAppInForeground_whenItGetsEncoded_thenTheValueIsPreservedAfterDecoding() throws { + let randomIsAppInForeground: Bool = .mockRandom() + + // Given + var context: CrashContext = .mockRandom() + context.lastIsAppInForeground = randomIsAppInForeground + + // When + let serializedContext = try encoder.encode(context) + + // Then + let deserializedContext = try decoder.decode(CrashContext.self, from: serializedContext) + XCTAssertEqual(deserializedContext.lastIsAppInForeground, randomIsAppInForeground) + } + // MARK: - Helpers /// Asserts that JSON representations of two `[String: Encodable]` dictionaries are equal. diff --git a/Tests/DatadogTests/Datadog/FeaturesIntegration/RUMWithCrashContextIntegrationTests.swift b/Tests/DatadogTests/Datadog/FeaturesIntegration/RUMWithCrashContextIntegrationTests.swift index f177f9b320..3042c18359 100644 --- a/Tests/DatadogTests/Datadog/FeaturesIntegration/RUMWithCrashContextIntegrationTests.swift +++ b/Tests/DatadogTests/Datadog/FeaturesIntegration/RUMWithCrashContextIntegrationTests.swift @@ -21,6 +21,19 @@ class RUMWithCrashContextIntegrationTests: XCTestCase { XCTAssertEqual(CrashReportingFeature.instance?.rumViewEventProvider.currentValue, randomRUMViewEvent) } + func testWhenCrashReportingIsEnabled_itUpdatesCrashContextWithRUMSessionState() throws { + // When + CrashReportingFeature.instance = .mockNoOp() + defer { CrashReportingFeature.instance?.deinitialize() } + + // Then + let rumWithCrashContextIntegration = try XCTUnwrap(RUMWithCrashContextIntegration()) + let randomRUMSessionState: RUMSessionState = .mockRandom() + rumWithCrashContextIntegration.update(lastRUMSessionState: randomRUMSessionState) + + XCTAssertEqual(CrashReportingFeature.instance?.rumSessionStateProvider.currentValue, randomRUMSessionState) + } + func testWhenCrashReportingIsNotEnabled_itCannotBeInitialized() { // When XCTAssertNil(CrashReportingFeature.instance) diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index 29b7cf4d85..362408d9e2 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -671,13 +671,26 @@ class AppStateListenerMock: AppStateListening, AnyMockable { } static func mockAny() -> Self { + return mockAppInForeground(since: .mockDecember15th2019At10AMUTC()) + } + + static func mockAppInForeground(since date: Date = Date()) -> Self { return .init( - history: .init( - initialState: .init(isActive: true, date: .mockDecember15th2019At10AMUTC()), - recentDate: .mockDecember15th2019At10AMUTC() - ) + history: .init(initialState: .init(isActive: true, date: date), recentDate: date) + ) + } + + static func mockAppInBackground(since date: Date = Date()) -> Self { + return .init( + history: .init(initialState: .init(isActive: false, date: date), recentDate: date) ) } + + static func mockRandom(since date: Date = Date()) -> Self { + return Bool.random() ? mockAppInForeground(since: date) : mockAppInBackground(since: date) + } + + func subscribe(_ subscriber: Observer) where Observer.ObservedValue == AppStateHistory {} } extension UserInfo: AnyMockable, RandomMockable { diff --git a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift index c2a17a1339..259ae46fe8 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CrashReportingFeatureMocks.swift @@ -90,14 +90,18 @@ extension CrashContext { lastUserInfo: UserInfo = .mockAny(), lastRUMViewEvent: RUMEvent? = nil, lastNetworkConnectionInfo: NetworkConnectionInfo? = .mockAny(), - lastCarrierInfo: CarrierInfo? = .mockAny() + lastCarrierInfo: CarrierInfo? = .mockAny(), + lastRUMSessionState: RUMSessionState? = .mockAny(), + lastIsAppInForeground: Bool = .mockAny() ) -> CrashContext { return CrashContext( lastTrackingConsent: lastTrackingConsent, lastUserInfo: lastUserInfo, lastRUMViewEvent: lastRUMViewEvent, lastNetworkConnectionInfo: lastNetworkConnectionInfo, - lastCarrierInfo: lastCarrierInfo + lastCarrierInfo: lastCarrierInfo, + lastRUMSessionState: lastRUMSessionState, + lastIsAppInForeground: lastIsAppInForeground ) } @@ -107,7 +111,9 @@ extension CrashContext { lastUserInfo: .mockRandom(), lastRUMViewEvent: .mockRandom(), lastNetworkConnectionInfo: .mockRandom(), - lastCarrierInfo: .mockRandom() + lastCarrierInfo: .mockRandom(), + lastRUMSessionState: .mockRandom(), + lastIsAppInForeground: .mockRandom() ) } diff --git a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift index c4304ec7af..88a7998ed8 100644 --- a/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift @@ -593,6 +593,16 @@ extension RUMContext { } } +extension RUMSessionState: AnyMockable, RandomMockable { + static func mockAny() -> RUMSessionState { + return .init(sessionUUID: .mockAny(), isInitialSession: .mockAny(), hasTrackedAnyView: .mockAny()) + } + + static func mockRandom() -> RUMSessionState { + return .init(sessionUUID: .mockRandom(), isInitialSession: .mockRandom(), hasTrackedAnyView: .mockRandom()) + } +} + // MARK: - RUMScope Mocks func mockNoOpSessionListerner() -> RUMSessionListener { @@ -616,6 +626,7 @@ extension RUMScopeDependencies { eventOutput: RUMEventOutput = RUMEventOutputMock(), rumUUIDGenerator: RUMUUIDGenerator = DefaultRUMUUIDGenerator(), dateCorrector: DateCorrectorType = DateCorrectorMock(), + crashContextIntegration: RUMWithCrashContextIntegration? = nil, onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListerner() ) -> RUMScopeDependencies { return RUMScopeDependencies( @@ -627,6 +638,7 @@ extension RUMScopeDependencies { eventOutput: eventOutput, rumUUIDGenerator: rumUUIDGenerator, dateCorrector: dateCorrector, + crashContextIntegration: crashContextIntegration, vitalCPUReader: SamplingBasedVitalReaderMock(), vitalMemoryReader: SamplingBasedVitalReaderMock(), vitalRefreshRateReader: ContinuousVitalReaderMock(), @@ -644,6 +656,7 @@ extension RUMScopeDependencies { eventOutput: RUMEventOutput? = nil, rumUUIDGenerator: RUMUUIDGenerator? = nil, dateCorrector: DateCorrectorType? = nil, + crashContextIntegration: RUMWithCrashContextIntegration? = nil, onSessionStart: @escaping RUMSessionListener = mockNoOpSessionListerner() ) -> RUMScopeDependencies { return RUMScopeDependencies( @@ -655,6 +668,7 @@ extension RUMScopeDependencies { eventOutput: eventOutput ?? self.eventOutput, rumUUIDGenerator: rumUUIDGenerator ?? self.rumUUIDGenerator, dateCorrector: dateCorrector ?? self.dateCorrector, + crashContextIntegration: crashContextIntegration ?? self.crashContextIntegration, vitalCPUReader: SamplingBasedVitalReaderMock(), vitalMemoryReader: SamplingBasedVitalReaderMock(), vitalRefreshRateReader: ContinuousVitalReaderMock(), diff --git a/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift index 46f4403461..4d94890a3a 100644 --- a/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift @@ -198,6 +198,16 @@ extension URL: AnyMockable, RandomMockable { } } +extension UUID: AnyMockable, RandomMockable { + static func mockAny() -> UUID { + return UUID() + } + + static func mockRandom() -> UUID { + return UUID() + } +} + extension String: AnyMockable, RandomMockable { static func mockAny() -> String { return "abc" diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift index f843366deb..b8abbda57d 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScopeTests.swift @@ -75,91 +75,109 @@ class RUMSessionScopeTests: XCTestCase { // MARK: - Background Events Tracking - func testGivenNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { + func testGivenAppInBackgroundAndNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { // Given - let currentTime = Date() + let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: .mockRandom(), // no matter if its initial session or not + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockAppInBackground(since: sessionStartTime) // app in background + ), samplingRate: 100, - startTime: currentTime, - backgroundEventTrackingEnabled: true + startTime: sessionStartTime, + backgroundEventTrackingEnabled: true // BET enabled ) - XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: true, canStartApplicationLaunchView: false) + let commandTime = sessionStartTime.addingTimeInterval(1) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true, canStartApplicationLaunchView: .mockRandom()) XCTAssertTrue(scope.process(command: command)) // Then XCTAssertEqual(scope.viewScopes.count, 1, "It should start background view scope") - XCTAssertEqual(scope.viewScopes[0].viewStartTime, currentTime) - XCTAssertEqual(scope.viewScopes[0].viewName, RUMSessionScope.Constants.backgroundViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMSessionScope.Constants.backgroundViewURL) + XCTAssertEqual(scope.viewScopes[0].viewStartTime, commandTime, "Background view should be started at command time") + XCTAssertEqual(scope.viewScopes[0].viewName, RUMOffViewEventsHandlingRule.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMOffViewEventsHandlingRule.Constants.backgroundViewURL) } - func testGivenNoActiveViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { + func testGivenAppInBackgroundAndNoActiveViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanStartBackgroundView_itCreatesBackgroundScope() { // Given - let currentTime = Date() + let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: .mockRandom(), // no matter if its initial session or not + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockAppInBackground(since: sessionStartTime) // app in background + ), samplingRate: 100, - startTime: currentTime, - backgroundEventTrackingEnabled: true + startTime: sessionStartTime, + backgroundEventTrackingEnabled: true // BET enabled ) - _ = scope.process(command: RUMStartViewCommand.mockWith(time: currentTime, identity: "view")) + + var commandTime = sessionStartTime.addingTimeInterval(1) + _ = scope.process(command: RUMStartViewCommand.mockWith(time: commandTime, identity: "view")) _ = scope.process(command: RUMStartResourceCommand.mockAny()) - _ = scope.process(command: RUMStopViewCommand.mockWith(time: currentTime.addingTimeInterval(1), identity: "view")) + _ = scope.process(command: RUMStopViewCommand.mockWith(time: commandTime.addingTimeInterval(0.5), identity: "view")) - XCTAssertEqual(scope.viewScopes.count, 1, "It has one view scope...") + XCTAssertEqual(scope.viewScopes.count, 1, "There is one view scope...") XCTAssertFalse(scope.viewScopes[0].isActiveView, "... but the view is not active") // When - let command = RUMCommandMock(time: currentTime.addingTimeInterval(2), canStartBackgroundView: true, canStartApplicationLaunchView: false) + commandTime = commandTime.addingTimeInterval(1) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true, canStartApplicationLaunchView: .mockRandom()) XCTAssertTrue(scope.process(command: command)) // Then XCTAssertEqual(scope.viewScopes.count, 2, "It should start background view scope") - XCTAssertEqual(scope.viewScopes[1].viewStartTime, currentTime.addingTimeInterval(2)) - XCTAssertEqual(scope.viewScopes[1].viewName, RUMSessionScope.Constants.backgroundViewName) - XCTAssertEqual(scope.viewScopes[1].viewPath, RUMSessionScope.Constants.backgroundViewURL) + XCTAssertEqual(scope.viewScopes[1].viewStartTime, commandTime, "Background view should be started at command time") + XCTAssertEqual(scope.viewScopes[1].viewName, RUMOffViewEventsHandlingRule.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[1].viewPath, RUMOffViewEventsHandlingRule.Constants.backgroundViewURL) } - func testGivenNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { + func testGivenAppInBackgroundAndNoViewScopeAndBackgroundEventsTrackingEnabled_whenCommandCanNotStartBackgroundView_itDoesNotCreateBackgroundScope() { // Given - let currentTime = Date() + let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: .mockRandom(), // no matter if its initial session or not + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockAppInBackground(since: sessionStartTime) // app in background + ), samplingRate: 100, - startTime: currentTime, - backgroundEventTrackingEnabled: true + startTime: sessionStartTime, + backgroundEventTrackingEnabled: true // BET enabled ) - XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: false, canStartApplicationLaunchView: false) + let commandTime = sessionStartTime.addingTimeInterval(1) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: false, canStartApplicationLaunchView: .mockRandom()) XCTAssertTrue(scope.process(command: command)) // Then XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") } - func testGivenNoViewScopeAndBackgroundEventsTrackingDisabled_whenReceivingAnyCommand_itNeverCreatesBackgroundScope() { + func testGivenAppInAnyStateAndNoViewScopeAndBackgroundEventsTrackingDisabled_whenReceivingAnyCommand_itDoesNotCreateBackgroundScope() { // Given - let currentTime = Date() + let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: .mockRandom(), // no matter if its initial session or not + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockRandom(since: sessionStartTime) // no matter of app state (if foreground or background) + ), samplingRate: 100, - startTime: currentTime, - backgroundEventTrackingEnabled: false + startTime: sessionStartTime, + backgroundEventTrackingEnabled: false // BET disabled ) - XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When - let command = RUMCommandMock(time: currentTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: false) + let commandTime = sessionStartTime.addingTimeInterval(1) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: false) XCTAssertTrue(scope.process(command: command)) // Then @@ -168,57 +186,66 @@ class RUMSessionScopeTests: XCTestCase { // MARK: - Application Launch Events Tracking - func testGivenInitialSessionWithNoViewTrackedBefore_whenCommandCanStartApplicationLaunchView_itCreatesAppLaunchScope() { + func testGivenAppInForegroundAndInitialSessionWithNoViewTrackedBefore_whenCommandCanStartApplicationLaunchView_itCreatesAppLaunchScope() { // Given let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: true, + isInitialSession: true, // initial session parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockAppInForeground(since: sessionStartTime) // app in foreground + ), samplingRate: 100, startTime: sessionStartTime, backgroundEventTrackingEnabled: .mockRandom() // no matter of BET state ) - XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: false, canStartApplicationLaunchView: true) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom(), 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, sessionStartTime, "Application launch view should start at session start time") - XCTAssertEqual(scope.viewScopes[0].viewName, RUMSessionScope.Constants.applicationLaunchViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMSessionScope.Constants.applicationLaunchViewURL) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL) } - func testGivenNotInitialSessionWithNoViewTrackedBefore_whenCommandCanStartApplicationLaunchView_itDoesNotCreateAppLaunchScope() { + func testGivenAppInForegroundAndNotInitialSessionWithNoViewTrackedBefore_whenCommandCanStartApplicationLaunchView_itDoesNotCreateAppLaunchScope() { // Given let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: false, + isInitialSession: false, // not initial session parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockAppInForeground(since: sessionStartTime) // app in foreground + ), samplingRate: 100, startTime: sessionStartTime, backgroundEventTrackingEnabled: .mockRandom() // no matter of BET state ) - XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: false, canStartApplicationLaunchView: true) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: true) XCTAssertTrue(scope.process(command: command)) // Then XCTAssertTrue(scope.viewScopes.isEmpty, "It should not start any view scope") } - func testGivenAnySessionWithSomeViewsTrackedBefore_whenCommandCanStartApplicationLaunchView_itDoesNotCreateAppLaunchScope() { + func testGivenAppInAnyStateAndAnySessionWithSomeViewsTrackedBefore_whenCommandCanStartApplicationLaunchView_itDoesNotCreateAppLaunchScope() { // Given let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: .mockRandom(), // any session, no matter if initial or not + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, + dependencies: .mockWith( + appStateListener: AppStateListenerMock.mockRandom(since: sessionStartTime) // no matter of app state (if foreground or background) + ), samplingRate: 100, startTime: sessionStartTime, backgroundEventTrackingEnabled: .mockRandom() // no matter of BET state @@ -226,8 +253,9 @@ class RUMSessionScopeTests: XCTestCase { let commandsTime = sessionStartTime.addingTimeInterval(1) _ = scope.process(command: RUMStartViewCommand.mockWith(time: commandsTime, identity: "view")) + XCTAssertFalse(scope.viewScopes.isEmpty, "There is some view scope") _ = scope.process(command: RUMStopViewCommand.mockWith(time: commandsTime.addingTimeInterval(1), identity: "view")) - XCTAssertTrue(scope.viewScopes.isEmpty) + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When let command = RUMCommandMock(time: commandsTime.addingTimeInterval(2), canStartBackgroundView: false, canStartApplicationLaunchView: true) @@ -246,15 +274,13 @@ class RUMSessionScopeTests: XCTestCase { isInitialSession: true, // initial session parent: parent, dependencies: .mockWith( - appStateListener: AppStateListenerMock( - history: .init(initialState: .init(isActive: true, date: sessionStartTime), recentDate: sessionStartTime) // app in foreground - ) + appStateListener: AppStateListenerMock.mockAppInForeground(since: sessionStartTime) // app in foreground ), samplingRate: 100, startTime: sessionStartTime, backgroundEventTrackingEnabled: true // BET enabled ) - XCTAssertTrue(scope.viewScopes.isEmpty, "No views tracked before") + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When let commandTime = sessionStartTime.addingTimeInterval(1) @@ -264,26 +290,24 @@ class RUMSessionScopeTests: XCTestCase { // Then XCTAssertEqual(scope.viewScopes.count, 1, "It should start application launch view scope") XCTAssertEqual(scope.viewScopes[0].viewStartTime, sessionStartTime, "Application launch view should start at session start time") - XCTAssertEqual(scope.viewScopes[0].viewName, RUMSessionScope.Constants.applicationLaunchViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMSessionScope.Constants.applicationLaunchViewURL) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMOffViewEventsHandlingRule.Constants.applicationLaunchViewURL) } - func testGivenAppInBackgroundAndBETEnabledAndInitialSession_whenCommandCanStartBothApplicationLaunchAndBackgroundViews_itCreatesBackgroundScope() { + func testGivenAppInBackgroundAndBETEnabled_whenCommandCanStartBothApplicationLaunchAndBackgroundViews_itCreatesBackgroundScope() { // Given let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: true, // initial session + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, dependencies: .mockWith( - appStateListener: AppStateListenerMock( - history: .init(initialState: .init(isActive: false, date: sessionStartTime), recentDate: sessionStartTime) // app in background - ) + appStateListener: AppStateListenerMock.mockAppInBackground(since: sessionStartTime) // app in background ), samplingRate: 100, startTime: sessionStartTime, backgroundEventTrackingEnabled: true // BET enabled ) - XCTAssertTrue(scope.viewScopes.isEmpty, "No views tracked before") + XCTAssertTrue(scope.viewScopes.isEmpty, "There is no view scope") // When let commandTime = sessionStartTime.addingTimeInterval(1) @@ -293,20 +317,18 @@ class RUMSessionScopeTests: XCTestCase { // Then XCTAssertEqual(scope.viewScopes.count, 1, "It should start background view scope") XCTAssertEqual(scope.viewScopes[0].viewStartTime, commandTime, "Background view should be started at command time") - XCTAssertEqual(scope.viewScopes[0].viewName, RUMSessionScope.Constants.backgroundViewName) - XCTAssertEqual(scope.viewScopes[0].viewPath, RUMSessionScope.Constants.backgroundViewURL) + XCTAssertEqual(scope.viewScopes[0].viewName, RUMOffViewEventsHandlingRule.Constants.backgroundViewName) + XCTAssertEqual(scope.viewScopes[0].viewPath, RUMOffViewEventsHandlingRule.Constants.backgroundViewURL) } - func testGivenAppInBackgroundAndBETDisabledAndInitialSession_whenCommandCanStartBothApplicationLaunchAndBackgroundViews_itDoesNotCreateAnyScope() { + func testGivenAppInBackgroundAndBETDisabled_whenReceivingAnyCommand_itDoesNotCreateAnyScope() { // Given let sessionStartTime = Date() let scope: RUMSessionScope = .mockWith( - isInitialSession: true, // initial session + isInitialSession: .mockRandom(), // no matter if initial session or not parent: parent, dependencies: .mockWith( - appStateListener: AppStateListenerMock( - history: .init(initialState: .init(isActive: false, date: sessionStartTime), recentDate: sessionStartTime) // app in background - ) + appStateListener: AppStateListenerMock.mockAppInBackground(since: sessionStartTime) // app in background ), samplingRate: 100, startTime: sessionStartTime, @@ -316,7 +338,7 @@ class RUMSessionScopeTests: XCTestCase { // When let commandTime = sessionStartTime.addingTimeInterval(1) - let command = RUMCommandMock(time: commandTime, canStartBackgroundView: true, canStartApplicationLaunchView: true) + let command = RUMCommandMock(time: commandTime, canStartBackgroundView: .mockRandom(), canStartApplicationLaunchView: .mockRandom()) XCTAssertTrue(scope.process(command: command)) // Then @@ -325,7 +347,7 @@ class RUMSessionScopeTests: XCTestCase { // MARK: - Sampling - func testWhenSessionIsSampled_itDoesNotCreateViewScopes() { + func testWhenSessionIsRejected_itDoesNotCreateViewScopes() { let scope: RUMSessionScope = .mockWith(parent: parent, samplingRate: 0, startTime: Date()) XCTAssertEqual(scope.viewScopes.count, 0) XCTAssertTrue( @@ -335,6 +357,68 @@ class RUMSessionScopeTests: XCTestCase { XCTAssertEqual(scope.viewScopes.count, 0) } + // MARK: Integration with Crash Context + + func testWhenSessionScopeIsCreated_thenItUpdatesLastRUMSessionStateInCrashContext() throws { + let rumSessionStateProvider = ValuePublisher(initialValue: nil) + let randomIsInitialSession: Bool = .mockRandom() + + // When + let scope: RUMSessionScope = .mockWith( + isInitialSession: randomIsInitialSession, + parent: parent, + dependencies: .mockWith( + crashContextIntegration: RUMWithCrashContextIntegration( + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: rumSessionStateProvider + ) + ), + samplingRate: 50 + ) + + // Then + let rumSessionStateInjectedToCrashContext = try XCTUnwrap(rumSessionStateProvider.currentValue, "It must inject session state to crash context") + let expectedSessionState = RUMSessionState( + sessionUUID: scope.sessionUUID.rawValue, + isInitialSession: randomIsInitialSession, + hasTrackedAnyView: false + ) + XCTAssertEqual(rumSessionStateInjectedToCrashContext, expectedSessionState, "It must inject expected session state to crash context") + } + + func testWhenSessionScopeStartsAnyView_thenItUpdatesLastRUMSessionStateInCrashContext() throws { + let rumSessionStateProvider = ValuePublisher(initialValue: nil) + let randomIsInitialSession: Bool = .mockRandom() + + // Given + let sessionStartTime = Date() + let scope: RUMSessionScope = .mockWith( + isInitialSession: randomIsInitialSession, + parent: parent, + dependencies: .mockWith( + crashContextIntegration: RUMWithCrashContextIntegration( + rumViewEventProvider: .mockRandom(), + rumSessionStateProvider: rumSessionStateProvider + ) + ), + samplingRate: 100, + startTime: sessionStartTime + ) + + // When + _ = scope.process(command: RUMStartViewCommand.mockWith(time: sessionStartTime)) + XCTAssertFalse(scope.viewScopes.isEmpty, "Session started some view") + + // Then + let rumSessionStateInjectedToCrashContext = try XCTUnwrap(rumSessionStateProvider.currentValue, "It must inject session state to crash context") + let expectedSessionState = RUMSessionState( + sessionUUID: scope.sessionUUID.rawValue, + isInitialSession: randomIsInitialSession, + hasTrackedAnyView: true + ) + XCTAssertEqual(rumSessionStateInjectedToCrashContext, expectedSessionState, "It must inject expected session state to crash context") + } + // MARK: - Usage func testWhenNoActiveViewScopes_itLogsWarning() { diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index b1feb8de7d..964eaf8da0 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -1008,7 +1008,7 @@ class RUMViewScopeTests: XCTestCase { } } - // MARK: ViewScope counts Correction + // MARK: ViewScope Counts Correction func testGivenViewScopeWithDependentActionsResourcesErrors_whenDroppingEvents_thenCountsAreAdjusted() throws { struct ResourceMapperHolder { @@ -1139,4 +1139,38 @@ class RUMViewScopeTests: XCTestCase { 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") } + + // MARK: Integration with Crash Context + + func testWhenViewIsStarted_thenItUpdatesLastRUMViewEventInCrashContext() throws { + let rumViewEventProvider = ValuePublisher?>(initialValue: nil) + + // Given + let scope = RUMViewScope( + isInitialView: .mockRandom(), + parent: parent, + dependencies: dependencies.replacing( + crashContextIntegration: RUMWithCrashContextIntegration( + rumViewEventProvider: rumViewEventProvider, + rumSessionStateProvider: .mockAny() + ) + ), + identity: mockView, + path: "UIViewController", + name: "ViewController", + attributes: [:], + customTimings: [:], + startTime: Date() + ) + + // When + XCTAssertTrue( + scope.process(command: RUMStartViewCommand.mockWith(identity: mockView)) + ) + + // Then + let rumViewSent = try XCTUnwrap(output.recordedEvents(ofType: RUMEvent.self).last, "It should send view event") + let rumViewInjectedToCrashContext = try XCTUnwrap(rumViewEventProvider.currentValue, "It must inject view event to crash context") + XCTAssertEqual(rumViewSent, rumViewInjectedToCrashContext, "It must inject sent event to crash context") + } } diff --git a/Tests/DatadogTests/Datadog/Utils/SwiftExtensionsTests.swift b/Tests/DatadogTests/Datadog/Utils/SwiftExtensionsTests.swift index bd1cce1e1a..80daa9fb87 100644 --- a/Tests/DatadogTests/Datadog/Utils/SwiftExtensionsTests.swift +++ b/Tests/DatadogTests/Datadog/Utils/SwiftExtensionsTests.swift @@ -53,6 +53,13 @@ class TimeIntervalExtensionTests: XCTestCase { } } +class UUIDExtensionTests: XCTestCase { + func testNullUUID() { + let uuid: UUID = .nullUUID + XCTAssertEqual(uuid.uuidString, "00000000-0000-0000-0000-000000000000", "It must be all zeroes") + } +} + class IntegerOverflowExtensionTests: XCTestCase { func testHappyPath() { let reasonableDouble = Double(1_000.123_456) From c14df6ab2af2370185f69168e0401636964117e9 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 16 Dec 2021 17:31:45 +0100 Subject: [PATCH 2/2] RUMM-1765 Add separate unit tests for `RUMOffViewEventsHandlingRule` --- Datadog/Datadog.xcodeproj/project.pbxproj | 8 +- .../RUMOffViewEventsHandlingRuleTests.swift | 113 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRuleTests.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 0080f902d1..d411374ee2 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -313,6 +313,7 @@ 619E16E92578E73E00B2516B /* DataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16E82578E73E00B2516B /* DataMigrator.swift */; }; 619E16F12578E89700B2516B /* DeleteAllDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E16F02578E89700B2516B /* DeleteAllDataMigrator.swift */; }; 61A614E8276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */; }; + 61A614EA276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */; }; 61A763DC252DB2B3005A23F2 /* NSURLSessionBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */; }; 61A9238E256FCAA2009B9667 /* DateCorrectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A9238D256FCAA2009B9667 /* DateCorrectionTests.swift */; }; 61AADBDD263C7ECF008ABC6F /* EquatableInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AADBDC263C7ECF008ABC6F /* EquatableInTests.swift */; }; @@ -524,8 +525,8 @@ D24C27EA270C8BEE005DE596 /* DataCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24C27E9270C8BEE005DE596 /* DataCompression.swift */; }; D2791EF927170A760046E07A /* RUMSwiftUIScenarioTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */; }; D29889C9273413ED00A4D1A9 /* RUMViewsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */; }; - D2EFF3D32731822A00D09F33 /* RUMViewsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */; }; D29D5A4D273BF8B400A687C1 /* SwiftUIActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */; }; + D2EFF3D32731822A00D09F33 /* RUMViewsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */; }; D2F1B81126D795F3009F3293 /* DDNoopRUMMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81026D795F3009F3293 /* DDNoopRUMMonitor.swift */; }; D2F1B81326D8DA68009F3293 /* DDNoopRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81226D8DA68009F3293 /* DDNoopRUMMonitorTests.swift */; }; D2F1B81526D8E5FF009F3293 /* DDNoopTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */; }; @@ -985,6 +986,7 @@ 619E16E82578E73E00B2516B /* DataMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigrator.swift; sourceTree = ""; }; 619E16F02578E89700B2516B /* DeleteAllDataMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllDataMigrator.swift; sourceTree = ""; }; 61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOffViewEventsHandlingRule.swift; sourceTree = ""; }; + 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOffViewEventsHandlingRuleTests.swift; sourceTree = ""; }; 61A763D9252DB2B3005A23F2 /* DatadogTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DatadogTests-Bridging-Header.h"; sourceTree = ""; }; 61A763DA252DB2B3005A23F2 /* NSURLSessionBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionBridge.h; sourceTree = ""; }; 61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionBridge.m; sourceTree = ""; }; @@ -1194,8 +1196,8 @@ D24C27E9270C8BEE005DE596 /* DataCompression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCompression.swift; sourceTree = ""; }; D2791EF827170A760046E07A /* RUMSwiftUIScenarioTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSwiftUIScenarioTests.swift; sourceTree = ""; }; D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewsHandlerTests.swift; sourceTree = ""; }; - D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewsHandler.swift; sourceTree = ""; }; D29D5A4C273BF8B400A687C1 /* SwiftUIActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIActionModifier.swift; sourceTree = ""; }; + D2EFF3D22731822A00D09F33 /* RUMViewsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMViewsHandler.swift; sourceTree = ""; }; D2F1B81026D795F3009F3293 /* DDNoopRUMMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopRUMMonitor.swift; sourceTree = ""; }; D2F1B81226D8DA68009F3293 /* DDNoopRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopRUMMonitorTests.swift; sourceTree = ""; }; D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDNoopTracerTests.swift; sourceTree = ""; }; @@ -2490,6 +2492,7 @@ isa = PBXGroup; children = ( 61C1510C25AC8C1B00362D4B /* RUMViewIdentityTests.swift */, + 61A614E9276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift */, 617B953F24BF4DB300E6F443 /* RUMApplicationScopeTests.swift */, 61C2C20824C0C75500C0321C /* RUMSessionScopeTests.swift */, 6198D27024C6E3B700493501 /* RUMViewScopeTests.swift */, @@ -4196,6 +4199,7 @@ 6184751526EFCF1300C7C9C5 /* DatadogTestsObserver.swift in Sources */, 61133C602423990D00786299 /* RequestBuilderTests.swift in Sources */, 61133C572423990D00786299 /* FileReaderTests.swift in Sources */, + 61A614EA276B9D4C00A06CE7 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */, D2F1B81326D8DA68009F3293 /* DDNoopRUMMonitorTests.swift in Sources */, 61133C5F2423990D00786299 /* DataUploaderTests.swift in Sources */, 61D6FF7924E42A2900D0E375 /* DataUploadWorkerMock.swift in Sources */, diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRuleTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRuleTests.swift new file mode 100644 index 0000000000..3697c368fa --- /dev/null +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMOffViewEventsHandlingRuleTests.swift @@ -0,0 +1,113 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class RUMOffViewEventsHandlingRuleTests: XCTestCase { + // MARK: - When There Is No RUM Session + + func testWhenThereIsNoRUMSessionAndAppIsInForeground_itShouldHandleEventsInApplicationLaunchView() { + let rule = RUMOffViewEventsHandlingRule( + sessionState: nil, + isAppInForeground: true, + isBETEnabled: .mockRandom() + ) + XCTAssertEqual(rule, .handleInApplicationLaunchView, "It must start ApplicationLaunch view, because app is in foreground") + } + + func testWhenThereIsNoRUMSessionAndAppIsInBackground_itShouldHandleEventsInBackgroundView_onlyWhenBETIsEnabled() { + let rule1 = RUMOffViewEventsHandlingRule( + sessionState: nil, + isAppInForeground: false, + isBETEnabled: true + ) + XCTAssertEqual(rule1, .handleInBackgroundView, "It must start Background view") + + let rule2 = RUMOffViewEventsHandlingRule( + sessionState: nil, + isAppInForeground: false, + isBETEnabled: false + ) + XCTAssertEqual(rule2, .doNotHandle, "It must drop the event, because BET is disabled") + } + + // MARK: - When There Is RUM Session + + func testWhenThereIsRUMSessionAndAppIsInForeground_itShouldHandleEventsInApplicationLaunchView_onlyWhenInitialSessionWithNoViewsHistory() { + let rule1 = RUMOffViewEventsHandlingRule( + sessionState: .init( + sessionUUID: .mockRandom(), + isInitialSession: true, + hasTrackedAnyView: false + ), + isAppInForeground: true, + isBETEnabled: .mockRandom() + ) + XCTAssertEqual(rule1, .handleInApplicationLaunchView, "It must start ApplicationLaunch view") + + let rule2 = RUMOffViewEventsHandlingRule( + sessionState: .init( + sessionUUID: .mockRandom(), + isInitialSession: .mockRandom(), + hasTrackedAnyView: true + ), + isAppInForeground: true, + isBETEnabled: .mockRandom() + ) + XCTAssertEqual(rule2, .doNotHandle, "It must drop the event, because this session already tracked some views") + + let rule3 = RUMOffViewEventsHandlingRule( + sessionState: .init( + sessionUUID: .mockRandom(), + isInitialSession: false, + hasTrackedAnyView: .mockRandom() + ), + isAppInForeground: true, + isBETEnabled: .mockRandom() + ) + XCTAssertEqual(rule3, .doNotHandle, "It must drop the event, because this is not initial session") + } + + func testWhenThereIsRUMSessionAndAppIsInBackground_itShouldHandleEventsInBackgroundView_onlyWhenBETIsEnabled() { + let rule1 = RUMOffViewEventsHandlingRule( + sessionState: .init( + sessionUUID: .mockRandom(), + isInitialSession: .mockRandom(), + hasTrackedAnyView: .mockRandom() + ), + isAppInForeground: false, + isBETEnabled: true + ) + XCTAssertEqual(rule1, .handleInBackgroundView, "It must start Background view") + + let rule2 = RUMOffViewEventsHandlingRule( + sessionState: .init( + sessionUUID: .mockRandom(), + isInitialSession: .mockRandom(), + hasTrackedAnyView: .mockRandom() + ), + isAppInForeground: false, + isBETEnabled: false + ) + XCTAssertEqual(rule2, .doNotHandle, "It must drop the event, because BET is disabled") + } + + // MARK: - When There Is RUM Session But It Is Rejected By Sampler + + func testWhenThereIsRUMSessionButItIsRejectedBySampler_itShouldDropAllEvents() { + let rule = RUMOffViewEventsHandlingRule( + sessionState: .init( + sessionUUID: .nullUUID, // session is not sampled + isInitialSession: .mockRandom(), + hasTrackedAnyView: .mockRandom() + ), + isAppInForeground: .mockRandom(), + isBETEnabled: .mockRandom() + ) + XCTAssertEqual(rule, .doNotHandle, "It must drop the event, because session is not sampled") + } +}