Skip to content

Commit

Permalink
fix(Analytics): Updating session stop time for cached events. (#3405)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruisebas authored Jan 10, 2024
1 parent 0d8f12a commit 003e1f3
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public protocol AnalyticsClientBehaviour: Actor {
onSubmit: SubmitResult?)

@discardableResult func submitEvents() async throws -> [PinpointEvent]
func update(_ session: PinpointSession) async throws

nonisolated func createAppleMonetizationEvent(with transaction: SKPaymentTransaction,
with product: SKProduct) -> PinpointEvent
Expand Down Expand Up @@ -262,6 +263,10 @@ actor AnalyticsClient: AnalyticsClientBehaviour {
func submitEvents() async throws -> [PinpointEvent] {
return try await eventRecorder.submitAllEvents()
}

func update(_ session: PinpointSession) async throws {
try await eventRecorder.update(session)
}

func setAutomaticSubmitEventsInterval(_ interval: TimeInterval,
onSubmit: SubmitResult?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ protocol AnalyticsEventRecording: Actor {
func updateAttributesOfEvents(ofType: String,
withSessionId: PinpointSession.SessionId,
setAttributes: [String: String]) throws

/// Updates the session information of the events that match the same sessionId.
/// - Parameter session: The session to update
func update(_ session: PinpointSession) throws

/// Submit all locally stored events
/// - Returns: A collection of events submitted to Pinpoint
Expand Down Expand Up @@ -78,6 +82,10 @@ actor EventRecorder: AnalyticsEventRecording {
setAttributes: attributes)
}

func update(_ session: PinpointSession) throws {
try storage.updateSession(session)
}

/// Submit all locally stored events in batches. If a previous submission is in progress, it waits until it's completed before proceeding.
/// When the submission for an event is accepted, the event is removed from local storage
/// When the submission for an event is rejected, the event retry count is incremented in the local storage. Events that exceed the maximum retry count (3) are purged.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ class AnalyticsEventSQLStorage: AnalyticsEventStorage {
sessionId,
eventType])
}

func updateSession(_ session: PinpointSession) throws {
let updateStatement = """
UPDATE Event
SET sessionStartTime = ?, sessionStopTime = ?
WHERE sessionId = ?
"""
_ = try dbAdapter.executeQuery(
updateStatement,
[
session.startTime.asISO8601String,
session.stopTime?.asISO8601String,
session.sessionId
]
)
}

/// Get the oldest event with limit
/// - Parameter limit: The number of query result to limit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ protocol AnalyticsEventStorage {
func updateEvents(ofType: String,
withSessionId: PinpointSession.SessionId,
setAttributes: [String: String]) throws

/// Updates the session information of the events that match the same sessionId.
/// - Parameter session: The session to update
func updateSession(_ session: PinpointSession) throws

/// Get the oldest event with limit
/// - Parameter limit: The number of query result to limit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,36 @@ import AWSPluginsCore
import Foundation

extension PinpointEvent {
var clientTypeSession: PinpointClientTypes.Session? {
#if os(watchOS)
// If the session duration cannot be represented by Int, return a nil session instead.
// This is extremely unlikely to happen since a session's stopTime is set when the app is closed
if let duration = session.duration, duration > Int.max {
return nil
private var clientTypeSession: PinpointClientTypes.Session? {
var sessionDuration: Int? = nil
if let duration = session.duration {
// If the session duration cannot be represented by Int, return a nil session instead.
// This is extremely unlikely to happen since a session's stopTime is set when the app is closed
guard let intDuration = Int(exactly: duration) else { return nil }
sessionDuration = intDuration
}
#endif
return PinpointClientTypes.Session(duration: Int(session.duration),
id: session.sessionId,
startTimestamp: session.startTime.asISO8601String,
stopTimestamp: session.stopTime?.asISO8601String)

return .init(
duration: sessionDuration,
id: session.sessionId,
startTimestamp: session.startTime.asISO8601String,
stopTimestamp: session.stopTime?.asISO8601String
)
}

var clientTypeEvent: PinpointClientTypes.Event {
return PinpointClientTypes.Event(appPackageName: Bundle.main.appPackageName,
appTitle: Bundle.main.appName,
appVersionCode: Bundle.main.appVersion,
attributes: attributes,
clientSdkVersion: AmplifyAWSServiceConfiguration.amplifyVersion,
eventType: eventType,
metrics: metrics,
sdkName: AmplifyAWSServiceConfiguration.platformName,
session: clientTypeSession,
timestamp: eventDate.asISO8601String)
return .init(
appPackageName: Bundle.main.appPackageName,
appTitle: Bundle.main.appName,
appVersionCode: Bundle.main.appVersion,
attributes: attributes,
clientSdkVersion: AmplifyAWSServiceConfiguration.amplifyVersion,
eventType: eventType,
metrics: metrics,
sdkName: AmplifyAWSServiceConfiguration.platformName,
session: clientTypeSession,
timestamp: eventDate.asISO8601String
)
}
}

Expand All @@ -55,12 +60,3 @@ extension Bundle {
object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
}
}

private extension Int {
init?(_ value: Int64?) {
guard let value = value else {
return nil
}
self.init(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,79 @@
import Foundation

@_spi(InternalAWSPinpoint)
public class PinpointSession: Codable {
public struct PinpointSession: Codable {
private enum State: Codable {
case active
case paused(date: Date)
case stopped(date: Date)
}
typealias SessionId = String

let sessionId: SessionId
let startTime: Date
private(set) var stopTime: Date?
var stopTime: Date? {
switch state {
case .active:
return nil
case .paused(let stopTime),
.stopped(let stopTime):
return stopTime
}
}

private var state: State = .active

init(appId: String,
uniqueId: String) {
sessionId = Self.generateSessionId(appId: appId,
uniqueId: uniqueId)
startTime = Date()
stopTime = nil
}

init(sessionId: SessionId,
startTime: Date,
stopTime: Date?) {
self.sessionId = sessionId
self.startTime = startTime
self.stopTime = stopTime
if let stopTime {
self.state = .stopped(date: stopTime)
}
}

var isPaused: Bool {
return stopTime != nil
if case .paused = state {
return true
}

return false
}

var isStopped: Bool {
if case .stopped = state {
return true
}

return false
}

var duration: Date.Millisecond? {
/// According to Pinpoint's documentation, `duration` is only required if `stopTime` is not nil.
guard let endTime = stopTime else {
return nil
}
return endTime.millisecondsSince1970 - startTime.millisecondsSince1970
guard let stopTime else { return nil }
return stopTime.millisecondsSince1970 - startTime.millisecondsSince1970
}

func stop() {
guard stopTime == nil else { return }
stopTime = Date()
mutating func stop() {
guard !isStopped else { return }
state = .stopped(date: stopTime ?? Date())
}

func pause() {
mutating func pause() {
guard !isPaused else { return }
stopTime = Date()
state = .paused(date: Date())
}

func resume() {
stopTime = nil
mutating func resume() {
state = .active
}

private static func generateSessionId(appId: String,
Expand Down
Loading

0 comments on commit 003e1f3

Please sign in to comment.