diff --git a/Examples b/Examples index 577dcae69..37f1bf355 160000 --- a/Examples +++ b/Examples @@ -1 +1 @@ -Subproject commit 577dcae6938ce18501c38309a1fad3bde07c18d3 +Subproject commit 37f1bf3552f0330ff2b93c144f1ff6db88579e4e diff --git a/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift b/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift index b4917e1da..53a48c2f4 100644 --- a/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift +++ b/Sources/Core/InternalQueue/SessionControllerIQWrapper.swift @@ -59,6 +59,11 @@ class SessionControllerIQWrapper: SessionController { get { InternalQueue.sync { controller.onSessionStateUpdate } } set { InternalQueue.sync { controller.onSessionStateUpdate = newValue } } } + + var continueSessionOnRestart: Bool { + get { InternalQueue.sync { controller.continueSessionOnRestart } } + set { InternalQueue.sync { controller.continueSessionOnRestart = newValue } } + } var sessionIndex: Int { InternalQueue.sync { controller.sessionIndex } diff --git a/Sources/Core/Session/Session.swift b/Sources/Core/Session/Session.swift index 10daa9b65..c9700d5d7 100644 --- a/Sources/Core/Session/Session.swift +++ b/Sources/Core/Session/Session.swift @@ -21,11 +21,8 @@ class Session { // MARK: - Private properties private var dataPersistence: DataPersistence? - /// The event index - private var eventIndex = 0 private var isNewSession = true private var isSessionCheckerEnabled = false - private var lastSessionCheck: NSNumber = Utilities.getTimestamp() /// Returns the current session state private var state: SessionState? /// The current tracker associated with the session @@ -51,6 +48,8 @@ class Session { var sessionId: String? { return state?.sessionId } var previousSessionId: String? { return state?.previousSessionId } var firstEventId: String? { return state?.firstEventId } + /// If enabled, will persist all session updates (also changes to eventIndex) and will be able to continue the previous session when the app is closed and reopened. + var continueSessionOnRestart: Bool // MARK: - Constructor and destructor @@ -59,11 +58,20 @@ class Session { /// - foregroundTimeout: the session timeout while it is in the foreground /// - backgroundTimeout: the session timeout while it is in the background /// - tracker: reference to the associated tracker of the session + /// - continueSessionOnRestart: whether to resume previous persisted session /// - Returns: a SnowplowSession - init(foregroundTimeout: Int, backgroundTimeout: Int, trackerNamespace: String? = nil, tracker: Tracker? = nil) { + init( + foregroundTimeout: Int, + backgroundTimeout: Int, + trackerNamespace: String? = nil, + tracker: Tracker? = nil, + continueSessionOnRestart: Bool = false + ) { self.foregroundTimeout = foregroundTimeout * 1000 self.backgroundTimeout = backgroundTimeout * 1000 + self.continueSessionOnRestart = continueSessionOnRestart + self.isNewSession = !continueSessionOnRestart self.tracker = tracker if let namespace = trackerNamespace { dataPersistence = DataPersistence.getFor(namespace: namespace) @@ -73,7 +81,6 @@ class Session { if var storedSessionDict = storedSessionDict { storedSessionDict[kSPSessionUserId] = userId state = SessionState(storedState: storedSessionDict) - dataPersistence?.session = storedSessionDict } if state == nil { logDiagnostic(message: "No previous session info available") @@ -127,25 +134,27 @@ class Session { /// - firstEventTimestamp: Device created timestamp of the first event of the session /// - userAnonymisation: Whether to anonymise user identifiers /// - Returns: a SnowplowPayload containing the session dictionary - func getDictWithEventId(_ eventId: String?, eventTimestamp: Int64, userAnonymisation: Bool) -> [String : Any]? { + func getAndUpdateSessionForEvent(_ eventId: String?, eventTimestamp: Int64, userAnonymisation: Bool) -> [String : Any]? { var context: [String : Any]? = nil if isSessionCheckerEnabled { - if shouldUpdate() { - update(eventId: eventId, eventTimestamp: eventTimestamp) + if shouldStartNewSession() { + startNewSession(eventId: eventId, eventTimestamp: eventTimestamp) if let onSessionStateUpdate = onSessionStateUpdate, let state = state { DispatchQueue.global(qos: .default).async { onSessionStateUpdate(state) } } + // only persist session changes + if !continueSessionOnRestart { persist() } } - lastSessionCheck = Utilities.getTimestamp() } - eventIndex += 1 + state?.updateForNextEvent(isSessionCheckerEnabled: isSessionCheckerEnabled) + // persist every session update + if continueSessionOnRestart { persist() } context = state?.sessionContext - context?[kSPSessionEventIndex] = NSNumber(value: eventIndex) if userAnonymisation { // mask the user identifier @@ -180,38 +189,29 @@ class Session { return userId } - private func shouldUpdate() -> Bool { + private func shouldStartNewSession() -> Bool { if isNewSession { return true } - let lastAccess = lastSessionCheck.int64Value - let now = Utilities.getTimestamp().int64Value - let timeout = inBackground ? backgroundTimeout : foregroundTimeout - return now < lastAccess || Int(now - lastAccess) > timeout + if let state = state, let lastAccess = state.lastUpdate { + let now = Utilities.getTimestamp().int64Value + let timeout = inBackground ? backgroundTimeout : foregroundTimeout + return now < lastAccess || Int(now - lastAccess) > timeout + } + return true } - private func update(eventId: String?, eventTimestamp: Int64) { + private func startNewSession(eventId: String?, eventTimestamp: Int64) { isNewSession = false - let sessionIndex = (state?.sessionIndex ?? 0) + 1 - let eventISOTimestamp = Utilities.timestamp(toISOString: eventTimestamp) - state = SessionState( - firstEventId: eventId, - firstEventTimestamp: eventISOTimestamp, - currentSessionId: Utilities.getUUIDString(), - previousSessionId: state?.sessionId, - sessionIndex: sessionIndex, - userId: userId, - storage: "LOCAL_STORAGE") - var sessionToPersist = state?.sessionContext - // Remove previousSessionId if nil because dictionaries with nil values aren't plist serializable - // and can't be stored with SPDataPersistence. - if state?.previousSessionId == nil { - var sessionCopy = sessionToPersist - sessionCopy?.removeValue(forKey: kSPSessionPreviousId) - sessionToPersist = sessionCopy + if let state = state { + state.startNewSession(eventId: eventId, eventTimestamp: eventTimestamp) + } else { + state = SessionState(eventId: eventId, eventTimestamp: eventTimestamp) } - dataPersistence?.session = sessionToPersist - eventIndex = 0 + } + + private func persist() { + dataPersistence?.session = state?.dataToPersist } // MARK: - background and foreground notifications diff --git a/Sources/Core/Session/SessionControllerImpl.swift b/Sources/Core/Session/SessionControllerImpl.swift index 702c953c0..9e370f4f3 100644 --- a/Sources/Core/Session/SessionControllerImpl.swift +++ b/Sources/Core/Session/SessionControllerImpl.swift @@ -87,6 +87,17 @@ class SessionControllerImpl: Controller, SessionController { session?.backgroundTimeout = newValue * 1000 } } + + var continueSessionOnRestart: Bool { + get { + return session?.continueSessionOnRestart ?? TrackerDefaults.continueSessionOnRestart + } + set { + dirtyConfig.continueSessionOnRestart = newValue + session?.continueSessionOnRestart = newValue + } + } + var onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? { get { diff --git a/Sources/Core/Tracker/ServiceProvider.swift b/Sources/Core/Tracker/ServiceProvider.swift index b3ed43bc1..5bb3d5e44 100644 --- a/Sources/Core/Tracker/ServiceProvider.swift +++ b/Sources/Core/Tracker/ServiceProvider.swift @@ -290,6 +290,7 @@ class ServiceProvider: NSObject, ServiceProviderProtocol { tracker.sessionContext = trackerConfiguration.sessionContext tracker.foregroundTimeout = sessionConfiguration.foregroundTimeoutInSeconds tracker.backgroundTimeout = sessionConfiguration.backgroundTimeoutInSeconds + tracker.continueSessionOnRestart = sessionConfiguration.continueSessionOnRestart tracker.exceptionEvents = trackerConfiguration.exceptionAutotracking tracker.subject = subject tracker.base64Encoded = trackerConfiguration.base64Encoding diff --git a/Sources/Core/Tracker/Tracker.swift b/Sources/Core/Tracker/Tracker.swift index 681c7adf3..3af4aa498 100644 --- a/Sources/Core/Tracker/Tracker.swift +++ b/Sources/Core/Tracker/Tracker.swift @@ -209,6 +209,20 @@ class Tracker: NSObject { } } + private var _continueSessionOnRestart = TrackerDefaults.continueSessionOnRestart + public var continueSessionOnRestart: Bool { + get { + return _continueSessionOnRestart + } + set(continueSessionOnRestart) { + _continueSessionOnRestart = continueSessionOnRestart + if builderFinished && session != nil { + session?.continueSessionOnRestart = continueSessionOnRestart + } + } + } + + private var _lifecycleEvents = false /// Returns whether lifecyle events is enabled. /// - Returns: Whether background and foreground events are sent. @@ -299,7 +313,9 @@ class Tracker: NSObject { foregroundTimeout: foregroundTimeout, backgroundTimeout: backgroundTimeout, trackerNamespace: trackerNamespace, - tracker: self) + tracker: self, + continueSessionOnRestart: continueSessionOnRestart + ) } if autotrackScreenViews { @@ -588,9 +604,9 @@ class Tracker: NSObject { // Add session if let session = session { - if let sessionDict = session.getDictWithEventId(event.eventId.uuidString, - eventTimestamp: event.timestamp, - userAnonymisation: userAnonymisation) { + if let sessionDict = session.getAndUpdateSessionForEvent(event.eventId.uuidString, + eventTimestamp: event.timestamp, + userAnonymisation: userAnonymisation) { event.addContextEntity(SelfDescribingJson(schema: kSPSessionContextSchema, andDictionary: sessionDict)) } else { logDiagnostic(message: String(format: "Unable to get session context for eventId: %@", event.eventId.uuidString)) diff --git a/Sources/Core/Tracker/TrackerDefaults.swift b/Sources/Core/Tracker/TrackerDefaults.swift index b5e79f7f4..218ba3a4a 100644 --- a/Sources/Core/Tracker/TrackerDefaults.swift +++ b/Sources/Core/Tracker/TrackerDefaults.swift @@ -33,4 +33,5 @@ class TrackerDefaults { private(set) static var geoLocationContext = false private(set) static var screenEngagementAutotracking = true private(set) static var immersiveSpaceContext = true + private(set) static var continueSessionOnRestart = false } diff --git a/Sources/Core/TrackerConstants.swift b/Sources/Core/TrackerConstants.swift index ba712e2d4..0eceb1aa1 100644 --- a/Sources/Core/TrackerConstants.swift +++ b/Sources/Core/TrackerConstants.swift @@ -157,6 +157,7 @@ let kSPSessionFirstEventId = "firstEventId" let kSPSessionFirstEventTimestamp = "firstEventTimestamp" let kSPSessionEventIndex = "eventIndex" let kSPSessionAnonymousUserId = "00000000-0000-0000-0000-000000000000" +let ksSPSessionLastUpdate = "lastUpdate" // --- Geo-Location Context let kSPGeoLatitude = "latitude" diff --git a/Sources/Snowplow/Configurations/SessionConfiguration.swift b/Sources/Snowplow/Configurations/SessionConfiguration.swift index d2e1226ea..085f14a06 100644 --- a/Sources/Snowplow/Configurations/SessionConfiguration.swift +++ b/Sources/Snowplow/Configurations/SessionConfiguration.swift @@ -39,6 +39,11 @@ public protocol SessionConfigurationProtocol: AnyObject { /// The callback called everytime the session is updated. @objc var onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? { get set } + /// If enabled, will be able to continue the previous session when the app is closed and reopened (if it doesn't timeout). + /// Disabled by default, which means that every restart of the app starts a new session. + /// When enabled, every event will result in the session being updated in the UserDefaults. + @objc + var continueSessionOnRestart: Bool { get set } } /// This class represents the configuration from of the applications session. @@ -65,7 +70,7 @@ public class SessionConfiguration: SerializableConfiguration, SessionConfigurati /// The timeout set for the inactivity of app when in background. @objc public var backgroundTimeoutInSeconds: Int { - get { return _backgroundTimeoutInSeconds ?? sourceConfig?.backgroundTimeoutInSeconds ?? 1800 } + get { return _backgroundTimeoutInSeconds ?? sourceConfig?.backgroundTimeoutInSeconds ?? TrackerDefaults.backgroundTimeout } set { _backgroundTimeoutInSeconds = newValue } } @@ -73,10 +78,20 @@ public class SessionConfiguration: SerializableConfiguration, SessionConfigurati /// The timeout set for the inactivity of app when in foreground. @objc public var foregroundTimeoutInSeconds: Int { - get { return _foregroundTimeoutInSeconds ?? sourceConfig?.foregroundTimeoutInSeconds ?? 1800 } + get { return _foregroundTimeoutInSeconds ?? sourceConfig?.foregroundTimeoutInSeconds ?? TrackerDefaults.foregroundTimeout } set { _foregroundTimeoutInSeconds = newValue } } + private var _continueSessionOnRestart: Bool? + /// If enabled, will be able to continue the previous session when the app is closed and reopened (if it doesn't timeout). + /// Disabled by default, which means that every restart of the app starts a new session. + /// When enabled, every event will result in the session being updated in the UserDefaults. + @objc + public var continueSessionOnRestart: Bool { + get { return _continueSessionOnRestart ?? sourceConfig?.continueSessionOnRestart ?? TrackerDefaults.continueSessionOnRestart } + set { _continueSessionOnRestart = newValue } + } + private var _onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? /// The callback called everytime the session is updated. @objc @@ -153,6 +168,15 @@ public class SessionConfiguration: SerializableConfiguration, SessionConfigurati onSessionStateUpdate = value return self } + + /// If enabled, will be able to continue the previous session when the app is closed and reopened (if it doesn't timeout). + /// Disabled by default, which means that every restart of the app starts a new session. + /// When enabled, every event will result in the session being updated in the UserDefaults. + @objc + public func continueSessionOnRestart(_ value: Bool) -> Self { + self.continueSessionOnRestart = value + return self + } // MARK: - NSCopying diff --git a/Sources/Snowplow/Tracker/SessionState.swift b/Sources/Snowplow/Tracker/SessionState.swift index c8283a771..1ed442e0d 100644 --- a/Sources/Snowplow/Tracker/SessionState.swift +++ b/Sources/Snowplow/Tracker/SessionState.swift @@ -29,41 +29,64 @@ public class SessionState: NSObject, State { public private(set) var storage: String @objc public private(set) var userId: String + public private(set) var eventIndex: Int? + public private(set) var lastUpdate: Int64? var sessionContext: [String : Any] { - return sessionDictionary - } - private var sessionDictionary: [String : Any] = [:] - - class func buildSessionDictionary(withFirstEventId firstEventId: String?, firstEventTimestamp: String?, currentSessionId: String, previousSessionId: String?, sessionIndex: Int, userId: String, storage: String) -> [String : Any] { var dictionary: [String : Any] = [:] - dictionary[kSPSessionPreviousId] = previousSessionId ?? NSNull() - dictionary[kSPSessionId] = currentSessionId - dictionary[kSPSessionFirstEventId] = firstEventId - dictionary[kSPSessionFirstEventTimestamp] = firstEventTimestamp + + // required + dictionary[kSPSessionUserId] = userId + dictionary[kSPSessionId] = sessionId dictionary[kSPSessionIndex] = sessionIndex dictionary[kSPSessionStorage] = storage - dictionary[kSPSessionUserId] = userId + + // optional + if let previousSessionId = previousSessionId { + dictionary[kSPSessionPreviousId] = previousSessionId + } + if let firstEventId = firstEventId { + dictionary[kSPSessionFirstEventId] = firstEventId + } + if let firstEventTimestamp = firstEventTimestamp { + dictionary[kSPSessionFirstEventTimestamp] = firstEventTimestamp + } + if let eventIndex = eventIndex { + dictionary[kSPSessionEventIndex] = eventIndex + } + return dictionary + } + + var dataToPersist: [String : Any] { + var dictionary = sessionContext + + if let lastUpdate = lastUpdate { + dictionary[ksSPSessionLastUpdate] = lastUpdate + } + return dictionary } - init(firstEventId: String?, firstEventTimestamp: String?, currentSessionId: String, previousSessionId: String?, sessionIndex: Int, userId: String, storage: String) { + init( + firstEventId: String?, + firstEventTimestamp: String?, + currentSessionId: String, + previousSessionId: String?, + sessionIndex: Int, + userId: String, + storage: String, + eventIndex: Int? = nil, + lastUpdate: Int64? = nil + ) { self.firstEventId = firstEventId self.firstEventTimestamp = firstEventTimestamp - sessionId = currentSessionId + self.sessionId = currentSessionId self.previousSessionId = previousSessionId self.sessionIndex = sessionIndex self.userId = userId self.storage = storage - - sessionDictionary = SessionState.buildSessionDictionary( - withFirstEventId: firstEventId, - firstEventTimestamp: firstEventTimestamp, - currentSessionId: currentSessionId, - previousSessionId: previousSessionId, - sessionIndex: sessionIndex, - userId: userId, - storage: storage) + self.eventIndex = eventIndex + self.lastUpdate = lastUpdate } init?(storedState: [String : Any]) { @@ -86,14 +109,39 @@ public class SessionState: NSObject, State { firstEventTimestamp = storedState[kSPSessionFirstEventTimestamp] as? String storage = storedState[kSPSessionStorage] as? String ?? "LOCAL_STORAGE" - - sessionDictionary = SessionState.buildSessionDictionary( - withFirstEventId: firstEventId, - firstEventTimestamp: firstEventTimestamp, - currentSessionId: sessionId, - previousSessionId: previousSessionId, - sessionIndex: sessionIndex, - userId: userId, - storage: storage) + + eventIndex = storedState[kSPSessionEventIndex] as? Int + + lastUpdate = storedState[ksSPSessionLastUpdate] as? Int64 + } + + convenience init(eventId: String?, eventTimestamp: Int64) { + self.init( + firstEventId: eventId, + firstEventTimestamp: Utilities.timestamp(toISOString: eventTimestamp), + currentSessionId: Utilities.getUUIDString(), + previousSessionId: nil, + sessionIndex: 1, + userId: Utilities.getUUIDString(), + storage: "LOCAL_STORAGE", + lastUpdate: Utilities.getTimestamp().int64Value + ) + } + + func startNewSession(eventId: String?, eventTimestamp: Int64) { + self.previousSessionId = self.sessionId + self.sessionId = Utilities.getUUIDString() + self.sessionIndex = self.sessionIndex + 1 + self.eventIndex = 0 + self.firstEventId = eventId + self.firstEventTimestamp = Utilities.timestamp(toISOString: eventTimestamp) + self.lastUpdate = Utilities.getTimestamp().int64Value + } + + func updateForNextEvent(isSessionCheckerEnabled: Bool) { + self.eventIndex = (self.eventIndex ?? 0) + 1 + if isSessionCheckerEnabled { + self.lastUpdate = Utilities.getTimestamp().int64Value + } } } diff --git a/Tests/Configurations/TestTrackerConfiguration.swift b/Tests/Configurations/TestTrackerConfiguration.swift index bcff59bc8..b8e220017 100644 --- a/Tests/Configurations/TestTrackerConfiguration.swift +++ b/Tests/Configurations/TestTrackerConfiguration.swift @@ -117,7 +117,9 @@ class TestTrackerConfiguration: XCTestCase { let trackerConfig = TrackerConfiguration(appId: "appid") let sessionConfig = SessionConfiguration( foregroundTimeoutInSeconds: expectedForeground, - backgroundTimeoutInSeconds: expectedBackground) + backgroundTimeoutInSeconds: expectedBackground + ) + sessionConfig.continueSessionOnRestart = true let tracker = Snowplow.createTracker(namespace: "namespace", network: networkConfig, configurations: [trackerConfig, sessionConfig]) let foreground = tracker.session?.foregroundTimeoutInSeconds ?? 0 @@ -129,6 +131,8 @@ class TestTrackerConfiguration: XCTestCase { let backgroundMeasure = (tracker.session)?.backgroundTimeout XCTAssertEqual(Measurement(value: Double(expectedForeground), unit: UnitDuration.seconds), foregroundMeasure) XCTAssertEqual(Measurement(value: Double(expectedBackground), unit: UnitDuration.seconds), backgroundMeasure) + + XCTAssertTrue(tracker.session?.continueSessionOnRestart ?? false) } func testSessionControllerUnavailableWhenContextTurnedOff() { diff --git a/Tests/TestSession.swift b/Tests/TestSession.swift index e3b2afe76..8126ed87a 100644 --- a/Tests/TestSession.swift +++ b/Tests/TestSession.swift @@ -28,7 +28,7 @@ class TestSession: XCTestCase { func testInit() { let session = Session(foregroundTimeout: 600, backgroundTimeout: 300) XCTAssertTrue(!session.inBackground) - XCTAssertNotNil(session.getDictWithEventId("eventid-1", eventTimestamp: 1654496481346, userAnonymisation: false)) + XCTAssertNotNil(session.getAndUpdateSessionForEvent("eventid-1", eventTimestamp: 1654496481346, userAnonymisation: false)) XCTAssertTrue(session.sessionIndex ?? 0 >= 1) XCTAssertEqual(session.foregroundTimeout, 600000) XCTAssertEqual(session.backgroundTimeout, 300000) @@ -49,7 +49,7 @@ class TestSession: XCTestCase { func testFirstSession() { let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) - let sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) + let sessionContext = session.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) let sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -60,7 +60,7 @@ class TestSession: XCTestCase { func testForegroundEventsOnSameSession() { let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) - var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) + var sessionContext = session.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) var sessionIndex = session.sessionIndex ?? 0 let sessionId = sessionContext?[kSPSessionId] as? String XCTAssertEqual(1, sessionIndex) @@ -70,7 +70,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) - sessionContext = session.getDictWithEventId("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -80,7 +80,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) - sessionContext = session.getDictWithEventId("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -90,7 +90,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 3.1) - sessionContext = session.getDictWithEventId("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) sessionIndex = session.sessionIndex ?? 0 XCTAssertEqual(2, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -113,7 +113,7 @@ class TestSession: XCTestCase { let session = tracker.session session?.updateInBackground() - let sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) + let sessionContext = session?.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) let sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -142,7 +142,7 @@ class TestSession: XCTestCase { let sessionId = session?.sessionId - var sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) + var sessionContext = session?.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) var sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -152,7 +152,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) - sessionContext = session?.getDictWithEventId("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) + sessionContext = session?.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -162,7 +162,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 1) - sessionContext = session?.getDictWithEventId("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) + sessionContext = session?.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(1, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -172,7 +172,7 @@ class TestSession: XCTestCase { Thread.sleep(forTimeInterval: 2.1) - sessionContext = session?.getDictWithEventId("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) + sessionContext = session?.getAndUpdateSessionForEvent("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) sessionIndex = session?.sessionIndex ?? 0 XCTAssertEqual(2, sessionIndex) XCTAssertEqual(sessionIndex, sessionContext?[kSPSessionIndex] as? Int) @@ -195,7 +195,7 @@ class TestSession: XCTestCase { } let session = tracker.session - var sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481351, userAnonymisation: false) + var sessionContext = session?.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481351, userAnonymisation: false) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertEqual("2022-06-06T06:21:21.351Z", sessionContext?[kSPSessionFirstEventTimestamp] as? String) XCTAssertFalse(session!.inBackground) @@ -206,7 +206,7 @@ class TestSession: XCTestCase { session?.updateInBackground() Thread.sleep(forTimeInterval: 1.1) - sessionContext = session?.getDictWithEventId("event_2", eventTimestamp: 1654496481352, userAnonymisation: false) + sessionContext = session?.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481352, userAnonymisation: false) XCTAssertEqual(oldSessionId, sessionContext?[kSPSessionPreviousId] as? String) XCTAssertEqual("event_2", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertEqual("2022-06-06T06:21:21.352Z", sessionContext?[kSPSessionFirstEventTimestamp] as? String) @@ -218,7 +218,7 @@ class TestSession: XCTestCase { session?.updateInForeground() Thread.sleep(forTimeInterval: 1.1) - sessionContext = session?.getDictWithEventId("event_3", eventTimestamp: 1654496481353, userAnonymisation: false) + sessionContext = session?.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481353, userAnonymisation: false) XCTAssertEqual(oldSessionId, sessionContext?[kSPSessionPreviousId] as? String) XCTAssertEqual("event_3", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertEqual("2022-06-06T06:21:21.353Z", sessionContext?[kSPSessionFirstEventTimestamp] as? String) @@ -230,7 +230,7 @@ class TestSession: XCTestCase { session?.updateInBackground() Thread.sleep(forTimeInterval: 1.1) - sessionContext = session?.getDictWithEventId("event_4", eventTimestamp: 1654496481354, userAnonymisation: false) + sessionContext = session?.getAndUpdateSessionForEvent("event_4", eventTimestamp: 1654496481354, userAnonymisation: false) XCTAssertEqual(oldSessionId, sessionContext?[kSPSessionPreviousId] as? String) XCTAssertEqual("event_4", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertEqual("2022-06-06T06:21:21.354Z", sessionContext?[kSPSessionFirstEventTimestamp] as? String) @@ -242,7 +242,7 @@ class TestSession: XCTestCase { func testTimeoutSessionWhenPauseAndResume() { let session = Session(foregroundTimeout: 1, backgroundTimeout: 1) - var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481355, userAnonymisation: false) + var sessionContext = session.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481355, userAnonymisation: false) var prevSessionId = sessionContext?[kSPSessionId] as? String XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertEqual("2022-06-06T06:21:21.355Z", sessionContext?[kSPSessionFirstEventTimestamp] as? String) @@ -250,7 +250,7 @@ class TestSession: XCTestCase { session.stopChecker() Thread.sleep(forTimeInterval: 2) - sessionContext = session.getDictWithEventId("event_2", eventTimestamp: 1654496481356, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481356, userAnonymisation: false) XCTAssertEqual(1, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual(prevSessionId, sessionContext?[kSPSessionId] as? String) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) @@ -259,7 +259,7 @@ class TestSession: XCTestCase { session.startChecker() - sessionContext = session.getDictWithEventId("event_3", eventTimestamp: 1654496481357, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481357, userAnonymisation: false) XCTAssertEqual(2, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual(prevSessionId, sessionContext?[kSPSessionPreviousId] as? String) XCTAssertEqual("event_3", sessionContext?[kSPSessionFirstEventId] as? String) @@ -278,7 +278,7 @@ class TestSession: XCTestCase { } let session = tracker.session - let sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481361, userAnonymisation: false) + let sessionContext = session?.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481361, userAnonymisation: false) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertFalse(session!.inBackground) XCTAssertEqual(0, session?.backgroundIndex) @@ -310,7 +310,7 @@ class TestSession: XCTestCase { } let session = tracker.session - let sessionContext = session?.getDictWithEventId("event_1", eventTimestamp: 1654496481358, userAnonymisation: false) + let sessionContext = session?.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481358, userAnonymisation: false) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) XCTAssertFalse(session!.inBackground) XCTAssertEqual(0, session?.backgroundIndex) @@ -333,12 +333,12 @@ class TestSession: XCTestCase { func testNoEventsForLongTimeDontIncreaseIndexMultipleTimes() { let session = Session(foregroundTimeout: 1, backgroundTimeout: 1) - var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481359, userAnonymisation: false) + var sessionContext = session.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481359, userAnonymisation: false) XCTAssertEqual("event_1", sessionContext?[kSPSessionFirstEventId] as? String) Thread.sleep(forTimeInterval: 4) - sessionContext = session.getDictWithEventId("event_2", eventTimestamp: 1654496481360, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481360, userAnonymisation: false) XCTAssertEqual(2, sessionContext?[kSPSessionIndex] as? Int) XCTAssertEqual("event_2", sessionContext?[kSPSessionFirstEventId] as? String) } @@ -418,38 +418,74 @@ class TestSession: XCTestCase { func testIncrementsEventIndex() { let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) - var sessionContext = session.getDictWithEventId("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) + var sessionContext = session.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481346, userAnonymisation: false) XCTAssertEqual(1, sessionContext?[kSPSessionEventIndex] as? Int) Thread.sleep(forTimeInterval: 1) - sessionContext = session.getDictWithEventId("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481347, userAnonymisation: false) XCTAssertEqual(2, sessionContext?[kSPSessionEventIndex] as? Int) Thread.sleep(forTimeInterval: 1) - sessionContext = session.getDictWithEventId("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481348, userAnonymisation: false) XCTAssertEqual(3, sessionContext?[kSPSessionEventIndex] as? Int) Thread.sleep(forTimeInterval: 3.1) - sessionContext = session.getDictWithEventId("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) + sessionContext = session.getAndUpdateSessionForEvent("event_4", eventTimestamp: 1654496481349, userAnonymisation: false) XCTAssertEqual(1, sessionContext?[kSPSessionEventIndex] as? Int) } func testAnonymisesUserIdentifiers() { let session = Session(foregroundTimeout: 3, backgroundTimeout: 3) - _ = session.getDictWithEventId("event_1", eventTimestamp: 1654496481345, userAnonymisation: false) + _ = session.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481345, userAnonymisation: false) session.startNewSession() // create previous session ID reference - let withoutAnonymisation = session.getDictWithEventId("event_2", eventTimestamp: 1654496481346, userAnonymisation: false) + let withoutAnonymisation = session.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481346, userAnonymisation: false) XCTAssertNotEqual("00000000-0000-0000-0000-000000000000", withoutAnonymisation?[kSPSessionUserId] as? String) XCTAssertNotNil(withoutAnonymisation?[kSPSessionPreviousId]) - let withAnonymisation = session.getDictWithEventId("event_3", eventTimestamp: 1654496481347, userAnonymisation: true) + let withAnonymisation = session.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481347, userAnonymisation: true) XCTAssertEqual("00000000-0000-0000-0000-000000000000", withAnonymisation?[kSPSessionUserId] as? String) XCTAssertEqual(NSNull(), withAnonymisation?[kSPSessionPreviousId] as? NSNull) } + + func testStartsNewSessionOnRestartByDefault() { + let session1 = Session(foregroundTimeout: 3, backgroundTimeout: 3, trackerNamespace: "t1") + let firstSession = session1.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481345, userAnonymisation: false) + + let session2 = Session(foregroundTimeout: 3, backgroundTimeout: 3, trackerNamespace: "t1") + let secondSession = session2.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481345, userAnonymisation: false) + + XCTAssertNotEqual(firstSession?[kSPSessionId] as! String, secondSession?[kSPSessionId] as! String) + XCTAssertEqual(firstSession?[kSPSessionId] as! String, secondSession?[kSPSessionPreviousId] as! String) + } + + func testResumesPreviouslyPersistedSessionIfEnabled() { + let session1 = Session(foregroundTimeout: 3, backgroundTimeout: 3, trackerNamespace: "t1", continueSessionOnRestart: true) + _ = session1.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481345, userAnonymisation: false) + let firstSession = session1.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481346, userAnonymisation: false) + + let session2 = Session(foregroundTimeout: 3, backgroundTimeout: 3, trackerNamespace: "t1", continueSessionOnRestart: true) + let secondSession = session2.getAndUpdateSessionForEvent("event_3", eventTimestamp: 1654496481347, userAnonymisation: false) + + XCTAssertEqual(firstSession?[kSPSessionId] as! String, secondSession?[kSPSessionId] as! String) + XCTAssertEqual(secondSession?[kSPSessionEventIndex] as! Int, 3) + } + + func testStartsNewSessionOnRestartOnTimeout() { + let session1 = Session(foregroundTimeout: 1, backgroundTimeout: 1, trackerNamespace: "t1", continueSessionOnRestart: true) + let firstSession = session1.getAndUpdateSessionForEvent("event_1", eventTimestamp: 1654496481345, userAnonymisation: false) + + Thread.sleep(forTimeInterval: 2) + + let session2 = Session(foregroundTimeout: 1, backgroundTimeout: 1, trackerNamespace: "t1", continueSessionOnRestart: true) + let secondSession = session2.getAndUpdateSessionForEvent("event_2", eventTimestamp: 1654496481345, userAnonymisation: false) + + XCTAssertNotEqual(firstSession?[kSPSessionId] as! String, secondSession?[kSPSessionId] as! String) + XCTAssertEqual(firstSession?[kSPSessionId] as! String, secondSession?[kSPSessionPreviousId] as! String) + } // Service methods