Skip to content

Commit

Permalink
♻️ Simplify session handling and update default timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
iujames committed Aug 17, 2023
1 parent c4cb757 commit f3cbb2c
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 57 deletions.
4 changes: 2 additions & 2 deletions Sources/AppcuesKit/Appcues+Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public extension Appcues {
UIDevice.identifier
}

var sessionTimeout: UInt = 1_800 // 30 minutes by default
var sessionTimeout: UInt = 300 // 5 minutes by default

var activityStorageMaxSize: UInt = 25

Expand Down Expand Up @@ -80,7 +80,7 @@ public extension Appcues {
}

/// Set the session timeout for the configuration. This timeout value is used to determine if a new session is started
/// upon the application returning to the foreground. The default value is 1800 seconds (30 minutes).
/// after a period of inactivity, in either foreground or background. The default value is 300 seconds (5 minutes).
/// - Parameter sessionTimeout: The timeout length, in seconds.
/// - Returns: The `Configuration` object.
@discardableResult
Expand Down
6 changes: 1 addition & 5 deletions Sources/AppcuesKit/Appcues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ public class Appcues: NSObject {
/// Clears out the current user in this session. Can be used when the user logs out of your application.
@objc
public func reset() {
// call this first to close final analytics on the session
sessionMonitor.reset()

storage.userID = ""
Expand Down Expand Up @@ -337,9 +336,6 @@ public class Appcues: NSObject {
// register the auto property decorator
let autoPropDecorator = container.resolve(AutoPropertyDecorator.self)
analyticsPublisher.register(decorator: autoPropDecorator)

// start session monitoring
sessionMonitor.start()
}

private func identify(isAnonymous: Bool, userID: String, properties: [String: Any]? = nil) {
Expand All @@ -355,7 +351,7 @@ public class Appcues: NSObject {
storage.userSignature = properties?.removeValue(forKey: "appcues:user_id_signature") as? String
if userChanged {
// when the identified user changes from last known value, we must start a new session
sessionMonitor.start()
sessionMonitor.reset()

// and clear any stored group information - will have to be reset as needed
storage.groupID = nil
Expand Down
40 changes: 28 additions & 12 deletions Sources/AppcuesKit/Data/Analytics/AnalyticsPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ internal class AnalyticsPublisher: AnalyticsPublishing {

private weak var appcues: Appcues?

private let sessionMonitor: SessionMonitoring

private var subscribers: [WeakAnalyticsSubscribing] = []
private var decorators: [WeakAnalyticsDecorating] = []

init(container: DIContainer) {
self.appcues = container.owner
self.sessionMonitor = container.resolve(SessionMonitoring.self)
}

func register(subscriber: AnalyticsSubscribing) {
Expand All @@ -46,8 +49,32 @@ internal class AnalyticsPublisher: AnalyticsPublishing {
}

func publish(_ update: TrackingUpdate) {
guard appcues?.isActive ?? false else { return }
let isSessionActive = appcues?.isActive ?? false

if !isSessionActive || sessionMonitor.isSessionExpired {
if sessionMonitor.start() {
// immediately track session started before any subsequent analytics
decorateAndPublish(
TrackingUpdate(
type: .event(name: SessionEvents.sessionStarted.rawValue, interactive: true),
properties: nil,
isInternal: true
)
)
} else {
// no session could be started (no user) and we cannot
// track anything
return
}
} else {
// we have a valid session, update its last activity timestamp to push out the timeout
sessionMonitor.updateLastActivity()
}

decorateAndPublish(update)
}

private func decorateAndPublish(_ update: TrackingUpdate) {
var update = update

// Apply decorations, removing any decorators that have been released from memory.
Expand Down Expand Up @@ -79,17 +106,6 @@ private extension AnalyticsPublisher {
}

extension AnalyticsPublishing {
// helper used for internal SDK events to allow for enum cases to be passed for the event name
func track<T>(_ item: T, properties: [String: Any]?, interactive: Bool) where T: RawRepresentable, T.RawValue == String {
publish(
TrackingUpdate(
type: .event(name: item.rawValue, interactive: interactive),
properties: properties,
isInternal: true
)
)
}

func screen(title: String) {
publish(TrackingUpdate(type: .screen(title), isInternal: true))
}
Expand Down
62 changes: 25 additions & 37 deletions Sources/AppcuesKit/Data/Analytics/SessionMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import Foundation
import UIKit

internal protocol SessionMonitoring: AnyObject {
func start()
var isSessionExpired: Bool { get }

func start() -> Bool
func updateLastActivity()
func reset()
}

internal enum SessionEvents: String, CaseIterable {
case sessionStarted = "appcues:session_started"
case sessionSuspended = "appcues:session_suspended"
case sessionResumed = "appcues:session_resumed"
case sessionReset = "appcues:session_reset"

static var allNames: [String] { allCases.map { $0.rawValue } }
}
Expand All @@ -27,26 +27,24 @@ internal class SessionMonitor: SessionMonitoring {

private weak var appcues: Appcues?
private let storage: DataStoring
private let publisher: AnalyticsPublishing
private let tracker: AnalyticsTracking

private let sessionTimeout: UInt

private var applicationBackgrounded: Date?
private var lastActivityAt: Date?

var isSessionExpired: Bool {
guard let lastActivityAt = lastActivityAt else { return false }
let elapsed = Int(Date().timeIntervalSince(lastActivityAt))
return elapsed >= sessionTimeout
}

init(container: DIContainer) {
self.appcues = container.owner
self.publisher = container.resolve(AnalyticsPublishing.self)
self.tracker = container.resolve(AnalyticsTracking.self)
self.storage = container.resolve(DataStoring.self)
self.sessionTimeout = container.resolve(Appcues.Config.self).sessionTimeout

NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didEnterBackground),
Expand All @@ -56,42 +54,32 @@ internal class SessionMonitor: SessionMonitoring {
}

// called on (A) sdk init (B) user identity change
func start() {
func start() -> Bool {
// if there is no user identified, we do not have a valid session
guard !storage.userID.isEmpty else { return }

guard !storage.userID.isEmpty else { return false }
appcues?.sessionID = UUID()
publisher.track(SessionEvents.sessionStarted, properties: nil, interactive: true)
updateLastActivity()
return true
}

func updateLastActivity() {
guard appcues?.sessionID != nil else { return }

lastActivityAt = Date()
}

// called on reset(), user sign-out
func reset() {
// this is interactive: true since a reset should flush to network immediately (with previous user ID)
// and the next session start will be sent in a new request, with the new user ID
publisher.track(SessionEvents.sessionReset, properties: nil, interactive: true)
appcues?.sessionID = nil
}
// ensure any pending in-memory analytics get processed asap
tracker.flush()

@objc
func applicationWillEnterForeground(notification: Notification) {
guard appcues?.sessionID != nil, let applicationBackgrounded = applicationBackgrounded else { return }

let elapsed = Int(Date().timeIntervalSince(applicationBackgrounded))
self.applicationBackgrounded = nil

if elapsed >= sessionTimeout {
appcues?.sessionID = UUID()
publisher.track(SessionEvents.sessionStarted, properties: nil, interactive: true)
} else {
publisher.track(SessionEvents.sessionResumed, properties: nil, interactive: false)
}
appcues?.sessionID = nil
lastActivityAt = nil
}

@objc
func didEnterBackground(notification: Notification) {
guard appcues?.sessionID != nil else { return }
applicationBackgrounded = Date()
publisher.track(SessionEvents.sessionSuspended, properties: nil, interactive: false)

// ensure any pending in-memory analytics get processed asap
tracker.flush()
Expand Down
2 changes: 2 additions & 0 deletions Sources/AppcuesKit/Presentation/Debugger/UIDebugger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ internal class UIDebugger: UIDebugging {
@objc
private func appcuesReset(notification: Notification) {
self.viewModel.reset()
self.viewModel.currentUserID = self.storage.userID
self.viewModel.isAnonymous = self.storage.isAnonymous
}
}

Expand Down
6 changes: 5 additions & 1 deletion Sources/AppcuesKit/Presentation/DeepLinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ internal class DeepLinkHandler: DeepLinkHandling {

func didHandleURL(_ url: URL) -> Bool {
guard let applicationID = config?.applicationID,
let action = Action(url: url, isSessionActive: container?.owner?.isActive ?? false, applicationID: applicationID) else {
let action = Action(
url: url,
isSessionActive: container?.owner?.isActive ?? false,
applicationID: applicationID
) else {
return false
}

Expand Down

0 comments on commit f3cbb2c

Please sign in to comment.