Skip to content
Draft
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
8 changes: 8 additions & 0 deletions Agent.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,9 @@
F85558532DE7C4F6008B6EDD /* SessionReplayData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85558522DE7C4F6008B6EDD /* SessionReplayData.swift */; };
F858F3602AE04B0C00CF9EB5 /* NRMAURLSessionHeaderTrackingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F858F35F2AE04B0C00CF9EB5 /* NRMAURLSessionHeaderTrackingTests.m */; };
F858F3612AE04B0C00CF9EB5 /* NRMAURLSessionHeaderTrackingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F858F35F2AE04B0C00CF9EB5 /* NRMAURLSessionHeaderTrackingTests.m */; };
F85CEE052ED0BD4D004A314F /* NRMASessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85CEE042ED0BD4D004A314F /* NRMASessionManager.swift */; };
F85CEE062ED0BD4D004A314F /* NRMASessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85CEE042ED0BD4D004A314F /* NRMASessionManager.swift */; };
F85CEE072ED0BD4D004A314F /* NRMASessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85CEE042ED0BD4D004A314F /* NRMASessionManager.swift */; };
F8672AB52EB11B2C0055FA51 /* UIFont+CSSConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8672AB42EB11B2C0055FA51 /* UIFont+CSSConversion.swift */; };
F86741072BD30F3F00DAA1A2 /* NRMAExceptionDataCollectionWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 02FF48D024DC622400115469 /* NRMAExceptionDataCollectionWrapper.m */; };
F8678AAE2CDBC62B008FD2A2 /* NRAutoCollectLogStressTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F8678AAC2CDBC62B008FD2A2 /* NRAutoCollectLogStressTest.m */; };
Expand Down Expand Up @@ -2605,6 +2608,7 @@
F848CDD52AA133FB0082052F /* NRMAInteractionEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NRMAInteractionEvent.h; sourceTree = "<group>"; };
F85558522DE7C4F6008B6EDD /* SessionReplayData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionReplayData.swift; sourceTree = "<group>"; };
F858F35F2AE04B0C00CF9EB5 /* NRMAURLSessionHeaderTrackingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NRMAURLSessionHeaderTrackingTests.m; sourceTree = "<group>"; };
F85CEE042ED0BD4D004A314F /* NRMASessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRMASessionManager.swift; sourceTree = "<group>"; };
F8672AB42EB11B2C0055FA51 /* UIFont+CSSConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+CSSConversion.swift"; sourceTree = "<group>"; };
F8678AAC2CDBC62B008FD2A2 /* NRAutoCollectLogStressTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NRAutoCollectLogStressTest.m; sourceTree = "<group>"; };
F8728E402ACC9D5A0056F641 /* NRMANetworkMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NRMANetworkMonitor.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3137,6 +3141,7 @@
children = (
02FF47F324DB56B000115469 /* NewRelicAgentInternal.h */,
02FF47F524DB56B000115469 /* NewRelicAgentInternal.m */,
F85CEE042ED0BD4D004A314F /* NRMASessionManager.swift */,
);
path = General;
sourceTree = "<group>";
Expand Down Expand Up @@ -5622,6 +5627,7 @@
02FF48E124DC622700115469 /* NRMAExceptionHandlerManager.m in Sources */,
02FF48BE24DC61F600115469 /* NRMAMethodProfiler.m in Sources */,
02FF49E124DC636100115469 /* NRMAHarvestableValue.m in Sources */,
F85CEE062ED0BD4D004A314F /* NRMASessionManager.swift in Sources */,
02FF480C24DB573E00115469 /* NRMAUserAction.m in Sources */,
02FF489D24DC61D200115469 /* NRMAURLSessionTaskOverride.m in Sources */,
F80C1D6A2DA07691007C0F52 /* ViewDetails.swift in Sources */,
Expand Down Expand Up @@ -6062,6 +6068,7 @@
02FF4C7E24E3201400115469 /* NRMAHarvestableAnalytics.m in Sources */,
02FF4C7F24E3201400115469 /* NRMAHexUploader.m in Sources */,
2B706116293F990D0097BDC4 /* NRMAURLSessionTaskSearch.m in Sources */,
F85CEE052ED0BD4D004A314F /* NRMASessionManager.swift in Sources */,
02FF4C8024E3201400115469 /* NRMADeviceInformation.m in Sources */,
02FF4C8124E3201400115469 /* NRMAInteractionHistoryObjCInterface.m in Sources */,
02FF4C8224E3201400115469 /* NRMAActivityTraceMeasurementProducer.m in Sources */,
Expand Down Expand Up @@ -6223,6 +6230,7 @@
3482326A2BC5F1970070FAC3 /* NRMAUUIDStore.m in Sources */,
348233992BC5F31E0070FAC3 /* NRMAGestureRecognizerInstrumentation.m in Sources */,
348232B02BC5F1F00070FAC3 /* NRMAKeyAttributes.m in Sources */,
F85CEE072ED0BD4D004A314F /* NRMASessionManager.swift in Sources */,
2B81C3512E99800F002F5593 /* UIVIewAssociations.swift in Sources */,
348232FF2BC5F2140070FAC3 /* HexUploadPublisher.mm in Sources */,
3482333C2BC5F2490070FAC3 /* NRMAClassDataContainer.m in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Agent/Analytics/NRMAAnalytics.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
- (NSString*) sessionAttributeJSONString;

- (void) sessionWillEnd;
- (void) newSession;
- (void) newSessionWithEndTimestamp:(NSDate *)endTimestamp;
- (void) newSessionWithStartTime:(long long) sessionStartTime;

//value is either a NSString or NSNumber;
Expand Down
21 changes: 10 additions & 11 deletions Agent/Analytics/NRMAAnalytics.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1132,38 +1132,37 @@ - (void) sessionWillEnd {
[[NewRelicAgentInternal sharedInstance].gestureFacade recordUserAction:backgroundGesture];
}

[self endSessionReusable];
}

- (void) newSession {
- (void) newSessionWithEndTimestamp:(NSDate *)endTimestamp {
_newSession = YES;
[self endSessionReusable];
[self endSessionReusableWithEndTimestamp:endTimestamp];
}

- (void) newSessionWithStartTime:(long long)sessionStartTime {
[self setSessionStartTime:sessionStartTime];
if(!([NRMAFlags shouldEnableNewEventSystem])) {
_analyticsController->newSessionWithStartTime(sessionStartTime);
}
[self newSession];
[self newSessionWithEndTimestamp:[NSDate date]];
}

- (void) endSessionReusable {
- (void)endSessionReusableWithEndTimestamp:(NSDate *)endTimestamp {
if([NRMAFlags shouldEnableNewEventSystem]){
if(![self addSessionEndAttribute]) { //has exception handling within
if(![self addSessionEndAttributeWithEndTimestamp:endTimestamp]) { //has exception handling within
NRLOG_AGENT_ERROR(@"failed to add session end attribute.");
}

if(![self addSessionEvent]) { //has exception handling within
if(![self addSessionEventWithEndTimestamp:endTimestamp]) { //has exception handling within
NRLOG_AGENT_ERROR(@"failed to add a session event");
}
}
else {
if(!_analyticsController->addSessionEndAttribute()) { //has exception handling within
if(!_analyticsController->addSessionEndAttribute([endTimestamp timeIntervalSince1970] * 1000)) {
NRLOG_AGENT_ERROR(@"failed to add session end attribute.");
}

if(!_analyticsController->addSessionEvent()) { //has exception handling within
if(!_analyticsController->addSessionEvent([endTimestamp timeIntervalSince1970] * 1000)) {
NRLOG_AGENT_ERROR(@"failed to add a session event");
}
}
Expand Down Expand Up @@ -1192,7 +1191,7 @@ -(void) handleHarvest {
[harvestableAnalytics release];
}

- (BOOL) addSessionEndAttribute {
- (BOOL) addSessionEndAttributeWithEndTimestamp:(NSDate *)endTimestamp {

NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:_sessionStartTime];

Expand All @@ -1204,7 +1203,7 @@ - (BOOL) addSessionEndAttribute {
return YES;
}

- (BOOL) addSessionEvent {
- (BOOL) addSessionEventWithEndTimestamp:(NSDate *)endTimestamp {
NRMASessionEvent *event = [[NRMASessionEvent alloc] initWithTimestamp:[NRMAAnalytics currentTimeMillis]
sessionElapsedTimeInSeconds:[[NSDate date] timeIntervalSinceDate:_sessionStartTime]
category:@"Session" withAttributeValidator:_attributeValidator];
Expand Down
157 changes: 157 additions & 0 deletions Agent/General/NRMASessionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// NRMASessionManager.swift
// NewRelicAgent
//
// Created by New Relic on 11/21/25.
// Copyright © 2025 New Relic. All rights reserved.
//

import Foundation

/// Manages mobile session lifecycle including:
/// - 4-hour maximum session duration
/// - 30-minute inactivity timeout
@objc public class NRMASessionManager: NSObject {

// MARK: - Singleton

/// Shared instance of the session manager
@objc public static let shared = NRMASessionManager()

// MARK: - Constants

private static let defaultMaxSessionDuration: TimeInterval = 4 * 60 * 60 // 4 hours in seconds
private static let defaultInactivityTimeout: TimeInterval = 30 * 60 // 30 minutes in seconds

// MARK: - Properties

/// The timestamp when the current session started (in milliseconds since epoch)
@objc public private(set) var sessionStartTimeMS: Int64 = 0

/// The timestamp of the last recorded user activity (in milliseconds since epoch)
@objc public private(set) var lastActivityTimeMS: Int64 = 0

@objc public private(set) var lastBackgroundTimeMS: Int64 = 0

/// Maximum session duration in seconds (default: 4 hours = 14400 seconds)
@objc public var maxSessionDuration: TimeInterval = defaultMaxSessionDuration

/// Inactivity timeout in seconds (default: 30 minutes = 1800 seconds)
@objc public var inactivityTimeout: TimeInterval = defaultInactivityTimeout

private let sessionQueue = DispatchQueue(label: "com.newrelic.sessionManager", attributes: [])

// MARK: - Session Lifecycle

/// Start a new session with the current timestamp
@objc public func startNewSession() {
let currentTime = NRMASessionManager.currentTimeMillis()
startNewSession(withStartTime: currentTime)
}

/// Start a new session with a specific start time
@objc public func startNewSession(withStartTime startTimeMS: Int64) {
sessionQueue.sync {
self.sessionStartTimeMS = startTimeMS
self.lastActivityTimeMS = startTimeMS
self.lastBackgroundTimeMS = 0

NRLOG_DEBUG("New session started at: \(startTimeMS)")
}
}

/// Record user activity
@objc public func recordActivity() {
sessionQueue.async {
self.lastActivityTimeMS = NRMASessionManager.currentTimeMillis()
NRLOG_DEBUG("Activity recorded at: \(self.lastActivityTimeMS)")
}
}

/// Record last background timestamp
@objc public func recordBackgroundTimestamp() {
sessionQueue.async {
self.lastBackgroundTimeMS = NRMASessionManager.currentTimeMillis()
NRLOG_DEBUG("Background timestamp recorded at: \(self.lastBackgroundTimeMS)")
}
}

// MARK: - Session Validation

/// Check if the current session should end due to duration or inactivity
/// - Returns: true if session should end, false otherwise
@objc public func shouldEndSession() -> Bool {
var shouldEnd = false
sessionQueue.sync {
shouldEnd = hasExceededMaxDuration() // || hasExceededInactivityTimeout()
}
return shouldEnd
}

/// Check if the current session should end due to background timeout
/// - Returns: true if session should end, false otherwise
@objc public func shouldEndSessionFromBackground() -> Bool {
var shouldEnd = false
sessionQueue.sync {
shouldEnd = hasExceededBackgroundTimeout()
}
return shouldEnd
}

/// Check if session has exceeded maximum duration
/// - Returns: true if duration exceeded, false otherwise
@objc public func hasExceededMaxDuration() -> Bool {
let currentTime = NRMASessionManager.currentTimeMillis()
let sessionDurationMS = currentTime - sessionStartTimeMS
let sessionDurationSeconds = TimeInterval(sessionDurationMS) / 1000.0

let exceeded = sessionDurationSeconds >= maxSessionDuration
if exceeded {
NRLOG_DEBUG(String(format: "Session exceeded max duration: %.2f seconds (limit: %.2f seconds)",
sessionDurationSeconds, maxSessionDuration))
}
return exceeded
}

/// Check if session has exceeded inactivity timeout
/// - Returns: true if inactivity exceeded, false otherwise
@objc public func hasExceededInactivityTimeout() -> Bool {
let currentTime = NRMASessionManager.currentTimeMillis()
let inactivityDurationMS = currentTime - lastActivityTimeMS
let inactivityDurationSeconds = TimeInterval(inactivityDurationMS) / 1000.0

let exceeded = inactivityDurationSeconds >= inactivityTimeout
if exceeded {
NRLOG_DEBUG(String(format: "Session exceeded inactivity timeout: %.2f seconds (limit: %.2f seconds)",
inactivityDurationSeconds, inactivityTimeout))
}
return exceeded
}

/// Check if session has exceeded background timeout
/// - Returns: true if background exceeded, false otherwise
@objc public func hasExceededBackgroundTimeout() -> Bool {
if lastBackgroundTimeMS == 0 {
return true
}
let currentTime = NRMASessionManager.currentTimeMillis()
let backgroundDurationMS = currentTime - lastBackgroundTimeMS
let backgroundDurationSeconds = TimeInterval(backgroundDurationMS) / 1000.0

let exceeded = backgroundDurationSeconds >= inactivityTimeout
if exceeded {
NRLOG_DEBUG(String(format: "Session exceeded background timeout: %.2f seconds (limit: %.2f seconds)",
backgroundDurationSeconds, inactivityTimeout))
}
return exceeded
}

// MARK: - Utility Methods

/// Get current time in milliseconds since epoch
@objc public static func currentTimeMillis() -> Int64 {
var time = timeval()
gettimeofday(&time, nil)
return Int64(time.tv_sec) * 1000 + Int64(time.tv_usec) / 1000
}
}
2 changes: 2 additions & 0 deletions Agent/General/NewRelicAgentInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL) collectNetworkErrors;
+ (BOOL) harvestNow;

- (void) endSessionWithTime:(NSTimeInterval) endTimestampSeconds;

// URLTransformer
+ (void)setURLTransformer:(NRMAURLTransformer *)urlTransformer;
+ (NRMAURLTransformer *)getURLTransformer;
Expand Down
Loading