Skip to content

Commit 2b5f03e

Browse files
RUM-17879 Merge session-activity reader into active-context reader and drop collector's session-scope dependency
1 parent 86220b4 commit 2b5f03e

6 files changed

Lines changed: 48 additions & 63 deletions

File tree

DatadogRUM/Sources/Feature/RUMFeature.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ internal final class RUMFeature: DatadogRemoteFeature, RUMSessionSamplerProvider
225225
)
226226

227227
timeseriesCollector?.activeContextReader = monitor
228-
timeseriesCollector?.sessionActivityReader = monitor
229228
self.timeseriesCollector = timeseriesCollector
230229

231230
if let refreshRateVital = dependencies.vitalsReaders?.refreshRate as? RenderLoopReader {

DatadogRUM/Sources/RUMMonitor/Monitor.swift

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,20 @@ internal typealias RUMErrorCategory = RUMErrorEvent.Error.Category
9898

9999
/// Exposes monitor state for readers that operate outside the `RUMCommand` pipeline
100100
/// (e.g. the timer-driven `TimeseriesSessionCollector`), which otherwise have no access to
101-
/// `command.globalAttributes` or the scope tree's active view.
101+
/// `command.globalAttributes`, the scope tree's active view, or `RUMSessionScope`'s own session
102+
/// lifetime rules.
102103
internal protocol RUMActiveContextReader: AnyObject {
103104
/// The current global attributes set through `addAttribute(forKey:value:)` / `addAttributes(_:)`.
104-
/// Safe to read from any thread.
105+
/// Conformers must guarantee this is safe to read from any thread.
105106
var globalAttributes: [AttributeKey: AttributeValue] { get }
106-
/// The currently active view, if any. Safe to read from any thread.
107+
/// The currently active view, if any. Conformers must guarantee this is safe to read from any thread.
107108
var activeView: (id: String?, path: String?, name: String?) { get }
108-
}
109-
110-
/// Exposes the active session's lifetime state for readers that operate outside the `RUMCommand` pipeline
111-
/// (e.g. the timer-driven `TimeseriesSessionCollector`), so they can evaluate `RUMSessionScope`'s own
112-
/// expiry rules against a live, single source of truth instead of maintaining a shadow copy of that state.
113-
internal protocol RUMSessionActivityReader: AnyObject {
114-
/// The active session's ID, start time, and time of last RUM interaction, or `nil` values if there is
115-
/// no active session. Safe to read from any thread.
116-
var sessionActivity: (sessionID: String?, sessionStartTime: Date?, lastInteractionTime: Date?) { get }
109+
/// Whether the session identified by `sessionID` has expired (exceeded its max duration or inactivity
110+
/// timeout) as of `date`, evaluated against a live, single source of truth instead of a shadow copy of
111+
/// that state. Returns `false` if `sessionID` doesn't match the currently active session (e.g. a session
112+
/// transition is still propagating), so callers should treat that as "skip the check for now", not
113+
/// "not expired". Conformers must guarantee this is safe to call from any thread.
114+
func isSessionExpired(sessionID: String, at date: Date) -> Bool
117115
}
118116

119117
internal class Monitor: RUMCommandSubscriber {
@@ -239,10 +237,17 @@ internal class Monitor: RUMCommandSubscriber {
239237
extension Monitor: RUMActiveContextReader {
240238
var globalAttributes: [AttributeKey: AttributeValue] { attributes }
241239
var activeView: (id: String?, path: String?, name: String?) { activeViewSnapshot }
242-
}
243240

244-
extension Monitor: RUMSessionActivityReader {
245-
var sessionActivity: (sessionID: String?, sessionStartTime: Date?, lastInteractionTime: Date?) { sessionActivitySnapshot }
241+
func isSessionExpired(sessionID: String, at date: Date) -> Bool {
242+
let activity = sessionActivitySnapshot
243+
guard activity.sessionID == sessionID,
244+
let sessionStartTime = activity.sessionStartTime,
245+
let lastInteractionTime = activity.lastInteractionTime else {
246+
return false
247+
}
248+
return RUMSessionScope.hasExpired(sessionStartTime: sessionStartTime, currentTime: date)
249+
|| RUMSessionScope.hasTimedOut(lastInteractionTime: lastInteractionTime, currentTime: date)
250+
}
246251
}
247252

248253
/// Declares `Monitor` conformance to public `RUMMonitorProtocol`.

DatadogRUM/Sources/RUMMonitor/Scopes/RUMSessionScope.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,11 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
1616
}
1717

1818
/// Whether a session is timed out due to inactivity, given the time of its last interaction.
19-
/// Shared with `TimeseriesSessionCollector`, which self-enforces this same rule from a live
20-
/// `RUMSessionActivityReader` snapshot instead of reacting to `RUMCommand`s.
2119
static func hasTimedOut(lastInteractionTime: Date, currentTime: Date) -> Bool {
2220
currentTime.timeIntervalSince(lastInteractionTime) >= Constants.sessionTimeoutDuration
2321
}
2422

2523
/// Whether a session has exceeded its maximum duration, given its start time.
26-
/// Shared with `TimeseriesSessionCollector`, which self-enforces this same rule from a live
27-
/// `RUMSessionActivityReader` snapshot instead of reacting to `RUMCommand`s.
2824
static func hasExpired(sessionStartTime: Date, currentTime: Date) -> Bool {
2925
currentTime.timeIntervalSince(sessionStartTime) >= Constants.sessionMaxDuration
3026
}
@@ -109,10 +105,8 @@ internal class RUMSessionScope: RUMScope, RUMContextProvider {
109105
/// If this is the very first session created in the current app process (`false` for session created upon expiration of a previous one).
110106
let isInitialSession: Bool
111107
/// The start time of this Session, measured in device date. In initial session this is the time of SDK init.
112-
/// Exposed internally (not `private`) so `Monitor` can snapshot it for `RUMSessionActivityReader`.
113108
let sessionStartTime: Date
114109
/// Time of the last RUM interaction noticed by this Session.
115-
/// Exposed internally (not `private`) so `Monitor` can snapshot it for `RUMSessionActivityReader`.
116110
private(set) var lastInteractionTime: Date
117111
/// Indicates whether the "ApplicationLaunch" view was active when the app entered the background.
118112
private var hadApplicationLaunchViewWhenEnteringBackground: Bool? = nil

DatadogRUM/Sources/Timeseries/TimeseriesSessionCollector.swift

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import DatadogInternal
99

1010
/// Defines the interface for collecting timeseries data during a RUM session.
1111
internal protocol TimeseriesCollecting: AnyObject {
12-
/// Provides global custom attributes and the active view at sample time. Set by `RUMFeature` once `Monitor`
13-
/// is constructed, since the collector is created before it.
12+
/// Provides global custom attributes, the active view, and the active session's expiry state at sample
13+
/// time, so the collector can self-enforce `RUMSessionScope`'s own expiry rules without maintaining a
14+
/// shadow copy of that state. Set by `RUMFeature` once `Monitor` is constructed, since the collector is
15+
/// created before it.
1416
var activeContextReader: RUMActiveContextReader? { get set }
15-
/// Provides the active session's start time and last-interaction time at sample time, so the collector can
16-
/// self-enforce `RUMSessionScope`'s own expiry rules without maintaining a shadow copy of that state.
17-
/// Set by `RUMFeature` once `Monitor` is constructed, since the collector is created before it.
18-
var sessionActivityReader: RUMSessionActivityReader? { get set }
1917
func start(sessionID: String, applicationID: String, sessionType: RUMSessionType)
2018
func pause(sessionID: String)
2119
func resume(sessionID: String)
@@ -61,8 +59,6 @@ internal class TimeseriesSessionCollector: TimeseriesCollecting {
6159

6260
/// `Monitor`'s conformance is safe to read from any thread.
6361
weak var activeContextReader: RUMActiveContextReader?
64-
/// `Monitor`'s conformance is safe to read from any thread.
65-
weak var sessionActivityReader: RUMSessionActivityReader?
6662

6763
private var memoryBuffer: [MemorySample] = []
6864
private var cpuBuffer: [CPUSample] = []
@@ -258,15 +254,9 @@ internal class TimeseriesSessionCollector: TimeseriesCollecting {
258254
// idle with no user interaction). This is a safety net only — it does not affect RUM's own
259255
// session state, it just stops this collector from uploading data past session expiry.
260256
//
261-
// Pulled fresh from `sessionActivityReader` on every tick, rather than from a locally pushed
257+
// Pulled fresh from `activeContextReader` on every tick, rather than from a locally pushed
262258
// copy, so there's a single live source of truth and no race with how/when that state is updated.
263-
// If the reader's session doesn't match (e.g. a session transition is still propagating), skip
264-
// the check for this tick rather than guessing.
265-
let activity = sessionActivityReader?.sessionActivity
266-
if let activity = activity, activity.sessionID == sessionID,
267-
let sessionStartTime = activity.sessionStartTime, let lastInteractionTime = activity.lastInteractionTime,
268-
RUMSessionScope.hasExpired(sessionStartTime: sessionStartTime, currentTime: currentDate)
269-
|| RUMSessionScope.hasTimedOut(lastInteractionTime: lastInteractionTime, currentTime: currentDate) {
259+
if activeContextReader?.isSessionExpired(sessionID: sessionID, at: currentDate) == true {
270260
timer?.cancel()
271261
timer = nil
272262
flushMemory()

DatadogRUM/Tests/RUMMonitor/Scopes/RUMSessionScopeTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,6 @@ class RUMSessionScopeTests: XCTestCase {
867867

868868
private class TimeseriesCollectorSpy: TimeseriesCollecting {
869869
weak var activeContextReader: RUMActiveContextReader?
870-
weak var sessionActivityReader: RUMSessionActivityReader?
871870
var startCallCount = 0
872871
var pauseCallCount = 0
873872
var resumeCallCount = 0

DatadogRUM/Tests/Timeseries/TimeseriesSessionCollectorTests.swift

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -820,12 +820,9 @@ class TimeseriesSessionCollectorTests: XCTestCase {
820820
totalRAM: 4_000_000_000,
821821
now: clock.now
822822
)
823-
let contextReader = RUMActiveContextReaderMock()
824-
collector.activeContextReader = contextReader
825-
826823
let startTime = clock.date
827-
let activityReader = RUMSessionActivityReaderMock(sessionID: "session-expired", sessionStartTime: startTime, lastInteractionTime: startTime)
828-
collector.sessionActivityReader = activityReader
824+
let contextReader = RUMActiveContextReaderMock(sessionID: "session-expired", sessionStartTime: startTime, lastInteractionTime: startTime)
825+
collector.activeContextReader = contextReader
829826
collector.start(sessionID: "session-expired", applicationID: "app-1", sessionType: .user)
830827

831828
let beforeExpiryExpectation = self.expectation(description: "samples collected before expiry")
@@ -838,7 +835,7 @@ class TimeseriesSessionCollectorTests: XCTestCase {
838835
// duration, mirroring how `Monitor` refreshes its snapshot on every processed command, without ever
839836
// calling stop() directly
840837
clock.date = startTime.addingTimeInterval(RUMSessionScope.Constants.sessionMaxDuration)
841-
activityReader.sessionActivity.lastInteractionTime = clock.date
838+
contextReader.sessionActivity.lastInteractionTime = clock.date
842839
let selfStopExpectation = self.expectation(description: "self-stop settled")
843840
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.3) { selfStopExpectation.fulfill() }
844841
waitForExpectations(timeout: 2)
@@ -868,12 +865,9 @@ class TimeseriesSessionCollectorTests: XCTestCase {
868865
totalRAM: 4_000_000_000,
869866
now: clock.now
870867
)
871-
let contextReader = RUMActiveContextReaderMock()
872-
collector.activeContextReader = contextReader
873-
874868
let startTime = clock.date
875-
let activityReader = RUMSessionActivityReaderMock(sessionID: "session-idle", sessionStartTime: startTime, lastInteractionTime: startTime)
876-
collector.sessionActivityReader = activityReader
869+
let contextReader = RUMActiveContextReaderMock(sessionID: "session-idle", sessionStartTime: startTime, lastInteractionTime: startTime)
870+
collector.activeContextReader = contextReader
877871
collector.start(sessionID: "session-idle", applicationID: "app-1", sessionType: .user)
878872

879873
let beforeTimeoutExpectation = self.expectation(description: "samples collected before timeout")
@@ -906,18 +900,15 @@ class TimeseriesSessionCollectorTests: XCTestCase {
906900
totalRAM: 4_000_000_000,
907901
now: clock.now
908902
)
909-
let contextReader = RUMActiveContextReaderMock()
910-
collector.activeContextReader = contextReader
911-
912903
let startTime = clock.date
913-
let activityReader = RUMSessionActivityReaderMock(sessionID: "session-active", sessionStartTime: startTime, lastInteractionTime: startTime)
914-
collector.sessionActivityReader = activityReader
904+
let contextReader = RUMActiveContextReaderMock(sessionID: "session-active", sessionStartTime: startTime, lastInteractionTime: startTime)
905+
collector.activeContextReader = contextReader
915906
collector.start(sessionID: "session-active", applicationID: "app-1", sessionType: .user)
916907

917908
// When — a RUM interaction is reported right before what would have been the inactivity timeout,
918909
// resetting the clock the reader exposes to `sample()`
919910
clock.date = startTime.addingTimeInterval(RUMSessionScope.Constants.sessionTimeoutDuration - 1)
920-
activityReader.sessionActivity.lastInteractionTime = clock.date
911+
contextReader.sessionActivity.lastInteractionTime = clock.date
921912
clock.date = clock.date.addingTimeInterval(RUMSessionScope.Constants.sessionTimeoutDuration - 1)
922913

923914
let expectation = self.expectation(description: "still sampling")
@@ -998,21 +989,28 @@ class TimeseriesSessionCollectorTests: XCTestCase {
998989
private class RUMActiveContextReaderMock: RUMActiveContextReader {
999990
var globalAttributes: [AttributeKey: AttributeValue]
1000991
var activeView: (id: String?, path: String?, name: String?)
992+
var sessionActivity: (sessionID: String?, sessionStartTime: Date?, lastInteractionTime: Date?)
1001993

1002994
init(
1003995
globalAttributes: [AttributeKey: AttributeValue] = [:],
1004-
activeView: (id: String?, path: String?, name: String?) = (.mockAny(), .mockAny(), nil)
996+
activeView: (id: String?, path: String?, name: String?) = (.mockAny(), .mockAny(), nil),
997+
sessionID: String? = nil,
998+
sessionStartTime: Date? = nil,
999+
lastInteractionTime: Date? = nil
10051000
) {
10061001
self.globalAttributes = globalAttributes
10071002
self.activeView = activeView
1003+
self.sessionActivity = (sessionID, sessionStartTime, lastInteractionTime)
10081004
}
1009-
}
10101005

1011-
private class RUMSessionActivityReaderMock: RUMSessionActivityReader {
1012-
var sessionActivity: (sessionID: String?, sessionStartTime: Date?, lastInteractionTime: Date?)
1013-
1014-
init(sessionID: String? = nil, sessionStartTime: Date? = nil, lastInteractionTime: Date? = nil) {
1015-
self.sessionActivity = (sessionID, sessionStartTime, lastInteractionTime)
1006+
func isSessionExpired(sessionID: String, at date: Date) -> Bool {
1007+
guard sessionActivity.sessionID == sessionID,
1008+
let sessionStartTime = sessionActivity.sessionStartTime,
1009+
let lastInteractionTime = sessionActivity.lastInteractionTime else {
1010+
return false
1011+
}
1012+
return RUMSessionScope.hasExpired(sessionStartTime: sessionStartTime, currentTime: date)
1013+
|| RUMSessionScope.hasTimedOut(lastInteractionTime: lastInteractionTime, currentTime: date)
10161014
}
10171015
}
10181016

0 commit comments

Comments
 (0)