Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUMM-1765 Collect RUM events during application launch - part 1 #677

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Datadog/DatadogConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
///
Expand Down
34 changes: 29 additions & 5 deletions Sources/Datadog/RUM/RUMMonitor/RUMCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ 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

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
Expand All @@ -29,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,
Expand All @@ -52,6 +54,8 @@ 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
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
Expand All @@ -60,6 +64,8 @@ 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
let canStartApplicationLaunchView = true // yes, we want to track errors in "ApplicationLaunch" view

/// The error message.
let message: String
Expand Down Expand Up @@ -116,6 +122,8 @@ 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
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.
Expand All @@ -140,6 +148,8 @@ 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
let canStartApplicationLaunchView = true // yes, we want to track resources in "ApplicationLaunch" view

/// Resource url
let url: String
Expand All @@ -157,6 +167,8 @@ 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`)
let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`)

/// Resource metrics.
let metrics: ResourceMetrics
Expand All @@ -166,6 +178,8 @@ 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`)
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
Expand All @@ -179,6 +193,8 @@ 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`)
let canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartResourceCommand`)

/// The error message.
let errorMessage: String
Expand Down Expand Up @@ -250,6 +266,8 @@ 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 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
Expand All @@ -259,6 +277,8 @@ 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 canStartApplicationLaunchView = false // no, we don't expect receiving it without an active view (started earlier on `RUMStartUserActionCommand`)

let actionType: RUMUserActionType
let name: String?
Expand All @@ -268,6 +288,8 @@ 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 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
Expand All @@ -278,6 +300,8 @@ 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 canStartApplicationLaunchView = true // yes, we want to track long tasks in "ApplicationLaunch" view (e.g. any hitches before presenting first UI)

let duration: TimeInterval
}
21 changes: 7 additions & 14 deletions Sources/Datadog/RUM/RUMMonitor/Scopes/RUMApplicationScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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
Expand All @@ -96,21 +93,17 @@ 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,
dependencies: dependencies,
samplingRate: samplingRate,
startTime: command.time,
backgroundEventTrackingEnabled: backgroundEventTrackingEnabled
)

sessionScope = initialSession
sessionScopeDidUpdate(initialSession)
_ = initialSession.process(command: startInitialViewCommand)
}

private func sessionScopeDidUpdate(_ sessionScope: RUMSessionScope) {
Expand Down
91 changes: 66 additions & 25 deletions Sources/Datadog/RUM/RUMMonitor/Scopes/RUMSessionScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,41 @@ 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

/// 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

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.
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.
Expand All @@ -40,6 +56,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
private var lastInteractionTime: Date

init(
isInitialSession: Bool,
parent: RUMContextProvider,
dependencies: RUMScopeDependencies,
samplingRate: Float,
Expand All @@ -51,6 +68,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
Expand All @@ -62,6 +80,7 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
startTime: Date
) {
self.init(
isInitialSession: false,
parent: expiredSession.parent,
dependencies: expiredSession.dependencies,
samplingRate: expiredSession.samplingRate,
Expand All @@ -75,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,
Expand Down Expand Up @@ -107,14 +127,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
// Consider starting an active view, "ApplicationLaunch" view or "Background" view
if let startViewCommand = command as? RUMStartViewCommand {
startView(on: startViewCommand)
} else if isInitialSession && !hasTrackedAnyView && command.canStartApplicationLaunchView {
startApplicationLaunchView(on: command)
} else if backgroundEventTrackingEnabled && !hasActiveView && command.canStartBackgroundView {
startBackgroundView(on: command)
}

// Propagate command
Expand All @@ -133,11 +152,18 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
return true
}

/// If there is an active view.
private var hasActiveView: Bool {
return viewScopes.contains { $0.isActiveView }
}

// 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,
Expand All @@ -150,22 +176,37 @@ 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 startApplicationLaunchView(on command: RUMCommand) {
viewScopes.append(
RUMViewScope(
isInitialView: true,
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) {
let isStartingInitialView = isInitialSession && !hasTrackedAnyView
viewScopes.append(
RUMViewScope(
isInitialView: isStartingInitialView,
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 {
Expand Down
Loading