Skip to content

Commit d8e6192

Browse files
RUM-13949 wire TimeseriesSessionCollector into RUM session and configuration
1 parent e347195 commit d8e6192

6 files changed

Lines changed: 139 additions & 6 deletions

File tree

DatadogRUM/Sources/Feature/RUMFeature.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,16 @@ internal final class RUMFeature: DatadogRemoteFeature {
184184
predicate: nextViewActionPredicate
185185
)
186186
},
187-
sessionType: configuration.sessionTypeOverride.flatMap { RUMSessionType(rawValue: $0) }
187+
sessionType: configuration.sessionTypeOverride.flatMap { RUMSessionType(rawValue: $0) },
188+
timeseriesCollector: {
189+
guard configuration.enableTimeseries, let vitalsReaders = configuration.vitalsUpdateFrequency.map({
190+
VitalsReaders(frequency: $0.timeInterval, telemetry: core.telemetry)
191+
}) else { return nil }
192+
return TimeseriesSessionCollector(
193+
memoryReader: vitalsReaders.memory,
194+
featureScope: featureScope
195+
)
196+
}()
188197
)
189198

190199
self.monitor = Monitor(

DatadogRUM/Sources/RUMConfiguration.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ extension RUM {
326326
/// Default: `false`.
327327
public var collectAccessibility: Bool
328328

329+
/// Enables collection of memory and CPU timeseries events.
330+
///
331+
/// When enabled, memory footprint and CPU usage are sampled every second and uploaded as
332+
/// timeseries events scoped to the RUM session. Requires `vitalsUpdateFrequency` to be set.
333+
///
334+
/// Default: `false`.
335+
public var enableTimeseries: Bool
336+
329337
/// Feature flags to preview features in RUM.
330338
public var featureFlags: FeatureFlags
331339

@@ -534,6 +542,7 @@ extension RUM.Configuration {
534542
/// - trackSlowFrames: Enables the collection of slow frames (view hitches). Default: `true`.
535543
/// - telemetrySampleRate: The sampling rate for SDK internal telemetry utilized by Datadog. Must be a value between `0` and `100`. Default: `20`.
536544
/// - collectAccessibility: Determines whether accessibility data should be collected and included in RUM view events. Default: `false`.
545+
/// - enableTimeseries: Enables collection of memory and CPU timeseries events. Default: `false`.
537546
/// - featureFlags: Experimental feature flags.
538547
///
539548
/// - Note: On watchOS, automatic UIKit and SwiftUI view/action tracking is unavailable. The predicate parameters will be ignored.
@@ -569,6 +578,7 @@ extension RUM.Configuration {
569578
trackSlowFrames: Bool = true,
570579
telemetrySampleRate: SampleRate = 20,
571580
collectAccessibility: Bool = false,
581+
enableTimeseries: Bool = false,
572582
featureFlags: FeatureFlags = .defaults
573583
) {
574584
self.applicationID = applicationID
@@ -598,6 +608,7 @@ extension RUM.Configuration {
598608
self.trackSlowFrames = trackSlowFrames
599609
self.telemetrySampleRate = telemetrySampleRate
600610
self.collectAccessibility = collectAccessibility
611+
self.enableTimeseries = enableTimeseries
601612
self.featureFlags = featureFlags
602613
}
603614
#else
@@ -624,6 +635,7 @@ extension RUM.Configuration {
624635
trackSlowFrames: Bool = true,
625636
telemetrySampleRate: SampleRate = 20,
626637
collectAccessibility: Bool = false,
638+
enableTimeseries: Bool = false,
627639
featureFlags: FeatureFlags = .defaults
628640
) {
629641
self.applicationID = applicationID
@@ -648,6 +660,7 @@ extension RUM.Configuration {
648660
self.trackSlowFrames = trackSlowFrames
649661
self.telemetrySampleRate = telemetrySampleRate
650662
self.collectAccessibility = collectAccessibility
663+
self.enableTimeseries = enableTimeseries
651664
self.featureFlags = featureFlags
652665
}
653666
#endif

DatadogRUM/Sources/RUMMonitor/Scopes/RUMScopeDependencies.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal struct RUMScopeDependencies {
4646
let firstFrameReader: RenderLoopReader
4747
let viewHitchesReaderFactory: () -> (RenderLoopReader & ViewHitchesModel)?
4848
let vitalsReaders: VitalsReaders?
49+
let timeseriesCollector: TimeseriesCollecting?
4950
let accessibilityReader: AccessibilityReading?
5051
let onSessionStart: RUM.SessionListener?
5152
let viewCache: ViewCache
@@ -98,7 +99,8 @@ internal struct RUMScopeDependencies {
9899
watchdogTermination: WatchdogTerminationMonitor?,
99100
networkSettledMetricFactory: @escaping (Date, String) -> TNSMetricTracking,
100101
interactionToNextViewMetricFactory: @escaping () -> INVMetricTracking?,
101-
sessionType: RUMSessionType?
102+
sessionType: RUMSessionType?,
103+
timeseriesCollector: TimeseriesCollecting? = nil
102104
) {
103105
self.featureScope = featureScope
104106
self.rumApplicationID = rumApplicationID
@@ -117,6 +119,7 @@ internal struct RUMScopeDependencies {
117119
self.firstFrameReader = firstFrameReader
118120
self.viewHitchesReaderFactory = viewHitchesReaderFactory
119121
self.vitalsReaders = vitalsReaders
122+
self.timeseriesCollector = timeseriesCollector
120123
self.accessibilityReader = accessibilityReader
121124
self.onSessionStart = onSessionStart
122125
self.viewCache = viewCache

DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
9494
/// Indicates whether the "ApplicationLaunch" view was active when the app entered the background.
9595
private var hadApplicationLaunchViewWhenEnteringBackground: Bool? = nil
9696
/// The reason why this session has ended or `nil` if it is still active.
97-
private(set) var endReason: EndReason?
97+
private(set) var endReason: EndReason? {
98+
didSet { if endReason != nil { dependencies.timeseriesCollector?.stop() } }
99+
}
98100

99101
/// Counter to track the index of views in this session. Starts at 0 for the first view.
100102
private var nextViewIndex: Int = 0
@@ -161,6 +163,12 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
161163

162164
// Update fatal error context with recent RUM session state:
163165
dependencies.fatalErrorContext.sessionState = state
166+
167+
dependencies.timeseriesCollector?.start(
168+
sessionID: sessionUUID.rawValue.uuidString.lowercased(),
169+
applicationID: dependencies.rumApplicationID,
170+
sessionType: dependencies.sessionType
171+
)
164172
}
165173

166174
/// Creates a new Session upon expiration of the previous one.

DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,4 +706,102 @@ class RUMSessionScopeTests: XCTestCase {
706706
// Then
707707
XCTAssertTrue(result)
708708
}
709+
710+
// MARK: - Timeseries collector lifecycle
711+
712+
func testWhenSessionScopeIsCreated_itStartsTimeseriesCollector() {
713+
// Given
714+
let collector = TimeseriesCollectorSpy()
715+
716+
// When
717+
let _: RUMSessionScope = .mockWith(
718+
parent: parent,
719+
dependencies: .mockWith(timeseriesCollector: collector)
720+
)
721+
722+
// Then
723+
XCTAssertEqual(collector.startCallCount, 1)
724+
XCTAssertEqual(collector.stopCallCount, 0)
725+
}
726+
727+
func testWhenSessionExpiresDueToMaxDuration_itStopsTimeseriesCollector() {
728+
// Given
729+
let collector = TimeseriesCollectorSpy()
730+
var currentTime = Date()
731+
let scope: RUMSessionScope = .mockWith(
732+
parent: parent,
733+
startTime: currentTime,
734+
dependencies: .mockWith(timeseriesCollector: collector)
735+
)
736+
737+
// When — push past the max session duration
738+
currentTime.addTimeInterval(RUMSessionScope.Constants.sessionMaxDuration)
739+
_ = scope.process(command: RUMCommandMock(time: currentTime), context: context, writer: writer)
740+
741+
// Then
742+
XCTAssertEqual(collector.stopCallCount, 1)
743+
}
744+
745+
func testWhenSessionExpiresDueToInactivity_itStopsTimeseriesCollector() {
746+
// Given
747+
let collector = TimeseriesCollectorSpy()
748+
var currentTime = Date()
749+
let scope: RUMSessionScope = .mockWith(
750+
parent: parent,
751+
startTime: currentTime,
752+
dependencies: .mockWith(timeseriesCollector: collector)
753+
)
754+
755+
_ = scope.process(command: RUMCommandMock(time: currentTime), context: context, writer: writer)
756+
757+
// When — push past the session inactivity timeout
758+
currentTime.addTimeInterval(RUMSessionScope.Constants.sessionTimeoutDuration)
759+
_ = scope.process(command: RUMCommandMock(time: currentTime), context: context, writer: writer)
760+
761+
// Then
762+
XCTAssertEqual(collector.stopCallCount, 1)
763+
}
764+
765+
func testWhenSessionScopeStartsNewSession_itStartsCollectorWithCorrectSessionID() {
766+
// Given
767+
let collector = TimeseriesCollectorSpy()
768+
let applicationID = "test-app-id"
769+
770+
// When
771+
let scope: RUMSessionScope = .mockWith(
772+
parent: parent,
773+
dependencies: .mockWith(
774+
rumApplicationID: applicationID,
775+
timeseriesCollector: collector
776+
)
777+
)
778+
779+
// Then
780+
XCTAssertEqual(collector.startCallCount, 1)
781+
XCTAssertEqual(collector.lastStartedApplicationID, applicationID)
782+
XCTAssertNotNil(collector.lastStartedSessionID, "Session ID should be set")
783+
XCTAssertFalse(collector.lastStartedSessionID?.isEmpty ?? true)
784+
XCTAssertEqual(collector.lastStartedSessionType, scope.context.sessionID != .nullUUID ? .user : .user)
785+
}
786+
}
787+
788+
// MARK: - Test Helpers
789+
790+
private class TimeseriesCollectorSpy: TimeseriesCollecting {
791+
var startCallCount = 0
792+
var stopCallCount = 0
793+
var lastStartedSessionID: String?
794+
var lastStartedApplicationID: String?
795+
var lastStartedSessionType: RUMSessionType?
796+
797+
func start(sessionID: String, applicationID: String, sessionType: RUMSessionType) {
798+
startCallCount += 1
799+
lastStartedSessionID = sessionID
800+
lastStartedApplicationID = applicationID
801+
lastStartedSessionType = sessionType
802+
}
803+
804+
func stop() {
805+
stopCallCount += 1
806+
}
709807
}

TestUtilities/Sources/Mocks/DatadogRUM/RUMFeatureMocks.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,8 @@ extension RUMScopeDependencies {
11531153
interactionToNextViewMetricFactory: @escaping () -> INVMetricTracking = {
11541154
INVMetric(predicate: TimeBasedINVActionPredicate())
11551155
},
1156-
sessionType: RUMSessionType? = nil
1156+
sessionType: RUMSessionType? = nil,
1157+
timeseriesCollector: TimeseriesCollecting? = nil
11571158
) -> RUMScopeDependencies {
11581159
return RUMScopeDependencies(
11591160
featureScope: featureScope,
@@ -1183,7 +1184,8 @@ extension RUMScopeDependencies {
11831184
watchdogTermination: watchdogTermination,
11841185
networkSettledMetricFactory: networkSettledMetricFactory,
11851186
interactionToNextViewMetricFactory: interactionToNextViewMetricFactory,
1186-
sessionType: sessionType
1187+
sessionType: sessionType,
1188+
timeseriesCollector: timeseriesCollector
11871189
)
11881190
}
11891191

@@ -1545,6 +1547,7 @@ public class RUMActionsHandlerMock: RUMActionsHandling {
15451547
onViewModifierTapped?(actionName, actionAttributes)
15461548
}
15471549
}
1550+
#endif
15481551

15491552
public class SamplingBasedVitalReaderMock: SamplingBasedVitalReader {
15501553
public var vitalData: Double?
@@ -1577,7 +1580,6 @@ public class ContinuousVitalReaderMock: ContinuousVitalReader {
15771580
}
15781581
}
15791582
}
1580-
#endif
15811583

15821584
extension TelemetryReceiver: AnyMockable {
15831585
public static func mockAny() -> Self { .mockWith() }

0 commit comments

Comments
 (0)