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

Add an option to continue previously persisted session when the app restarts rather than starting a new one #912

Open
wants to merge 2 commits into
base: release/6.1.0
Choose a base branch
from
Open
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 Examples
5 changes: 5 additions & 0 deletions Sources/Core/InternalQueue/SessionControllerIQWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
72 changes: 36 additions & 36 deletions Sources/Core/Session/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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]? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much better name!

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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions Sources/Core/Session/SessionControllerImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions Sources/Core/Tracker/ServiceProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions Sources/Core/Tracker/Tracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -299,7 +313,9 @@ class Tracker: NSObject {
foregroundTimeout: foregroundTimeout,
backgroundTimeout: backgroundTimeout,
trackerNamespace: trackerNamespace,
tracker: self)
tracker: self,
continueSessionOnRestart: continueSessionOnRestart
)
}

if autotrackScreenViews {
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions Sources/Core/Tracker/TrackerDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions Sources/Core/TrackerConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 27 additions & 2 deletions Sources/Snowplow/Configurations/SessionConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -65,18 +70,28 @@ 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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good spot

set { _backgroundTimeoutInSeconds = newValue }
}

private var _foregroundTimeoutInSeconds: Int?
/// 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
Expand Down Expand Up @@ -149,10 +164,20 @@ public class SessionConfiguration: SerializableConfiguration, SessionConfigurati
// MARK: - Builders

/// The callback called everytime the session is updated.
@objc
public func onSessionStateUpdate(_ value: ((_ sessionState: SessionState) -> Void)?) -> Self {
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this builder method have @objc annotation but the one above (onSessionStateUpdate()) doesn't?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I think we missed the @objc annotation on the onSessionStateUpdate builder function! Will add it here.

public func continueSessionOnRestart(_ value: Bool) -> Self {
self.continueSessionOnRestart = value
return self
}

// MARK: - NSCopying

Expand Down
Loading
Loading