Skip to content

Commit 3872d6f

Browse files
Merge pull request #916 from snowplow/release/6.2.0
Release/6.2.0
2 parents a6bcc21 + cc32d51 commit 3872d6f

16 files changed

+277
-132
lines changed

.github/workflows/build.yml

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,33 @@ jobs:
2424
fail-fast: false
2525
matrix:
2626
include:
27-
- name: "Integration (iOS, Xcode 13.4)"
28-
os: macos-12
29-
xcode-version: 13.4
30-
destination: "platform=iOS Simulator,name=iPhone 13"
27+
- name: "Integration (iOS, Xcode 14.1)"
28+
os: macos-13
29+
xcode-version: 14.1
30+
destination: "platform=iOS Simulator,name=iPhone 14"
3131
target: IntegrationTests
3232

33-
- name: "Unit (iOS, Xcode 13.4)"
34-
os: macos-12
35-
xcode-version: 13.4
36-
destination: "platform=iOS Simulator,name=iPhone 13"
33+
- name: "Unit (iOS, Xcode 14.1)"
34+
os: macos-13
35+
xcode-version: 14.1
36+
destination: "platform=iOS Simulator,name=iPhone 14"
3737
target: Tests
3838

39-
- name: "Unit (macOS, Xcode 13.4)"
40-
os: macos-12
41-
xcode-version: 13.4
39+
- name: "Unit (macOS, Xcode 14.1)"
40+
os: macos-13
41+
xcode-version: 14.1
4242
destination: "platform=macOS"
4343
target: Tests
4444

45-
- name: "Unit (watchOS, Xcode 13.4)"
46-
os: macos-12
47-
xcode-version: 13.4
48-
destination: "platform=watchOS Simulator,name=Apple Watch Series 7 - 45mm"
45+
- name: "Unit (watchOS, Xcode 14.1)"
46+
os: macos-13
47+
xcode-version: 14.1
48+
destination: "platform=watchOS Simulator,name=Apple Watch Series 8 (45mm)"
4949
target: Tests
5050

51-
- name: "Unit (tvOS, Xcode 13.4)"
52-
os: macos-12
53-
xcode-version: 13.4
51+
- name: "Unit (tvOS, Xcode 14.1)"
52+
os: macos-13
53+
xcode-version: 14.1
5454
destination: "platform=tvOS Simulator,name=Apple TV"
5555
target: Tests
5656

@@ -90,16 +90,8 @@ jobs:
9090
-scheme SnowplowTracker \
9191
-destination "${{ matrix.destination }}" \
9292
-only-testing ${{ matrix.target }} \
93-
-resultBundlePath TestResults \
9493
clean test | xcpretty
9594
96-
- name: Create test results
97-
uses: kishikawakatsumi/xcresulttool@v1
98-
with:
99-
path: TestResults.xcresult
100-
title: "Test results: ${{ matrix.name }}"
101-
if: success() || failure()
102-
10395
build_objc_demo_app:
10496
name: "ObjC demo (iOS ${{ matrix.version.ios }})"
10597
needs: test_framework

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
Version 6.2.0 (2025-02-11)
2+
--------------------------
3+
Add an option to continue previously persisted session when the app restarts rather than starting a new one (#912)
4+
Make SelfDescribingJson class open to allow for inheritance (#906) thanks to @TwoDollarsEsq
5+
16
Version 6.1.0 (2025-01-16)
27
--------------------------
38
Add new WebView interface (#913)

Examples

SnowplowTracker.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "SnowplowTracker"
3-
s.version = "6.1.0"
3+
s.version = "6.2.0"
44
s.summary = "Snowplow event tracker for iOS, macOS, tvOS, watchOS for apps and games."
55
s.description = <<-DESC
66
Snowplow is a mobile and event analytics platform with a difference: rather than tell our users how they should analyze their data, we deliver their event-level data in their own data warehouse, on their own Amazon Redshift or Postgres database, so they can analyze it any way they choose. Snowplow mobile is used by data-savvy games companies and app developers to better understand their users and how they engage with their games and applications. Snowplow is open source using the business-friendly Apache License, Version 2.0 and scales horizontally to many billions of events.

Sources/Core/InternalQueue/SessionControllerIQWrapper.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class SessionControllerIQWrapper: SessionController {
5959
get { InternalQueue.sync { controller.onSessionStateUpdate } }
6060
set { InternalQueue.sync { controller.onSessionStateUpdate = newValue } }
6161
}
62+
63+
var continueSessionOnRestart: Bool {
64+
get { InternalQueue.sync { controller.continueSessionOnRestart } }
65+
set { InternalQueue.sync { controller.continueSessionOnRestart = newValue } }
66+
}
6267

6368
var sessionIndex: Int {
6469
InternalQueue.sync { controller.sessionIndex }

Sources/Core/Session/Session.swift

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@ class Session {
2121
// MARK: - Private properties
2222

2323
private var dataPersistence: DataPersistence?
24-
/// The event index
25-
private var eventIndex = 0
2624
private var isNewSession = true
2725
private var isSessionCheckerEnabled = false
28-
private var lastSessionCheck: NSNumber = Utilities.getTimestamp()
2926
/// Returns the current session state
3027
private var state: SessionState?
3128
/// The current tracker associated with the session
@@ -51,6 +48,8 @@ class Session {
5148
var sessionId: String? { return state?.sessionId }
5249
var previousSessionId: String? { return state?.previousSessionId }
5350
var firstEventId: String? { return state?.firstEventId }
51+
/// 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.
52+
var continueSessionOnRestart: Bool
5453

5554
// MARK: - Constructor and destructor
5655

@@ -59,11 +58,20 @@ class Session {
5958
/// - foregroundTimeout: the session timeout while it is in the foreground
6059
/// - backgroundTimeout: the session timeout while it is in the background
6160
/// - tracker: reference to the associated tracker of the session
61+
/// - continueSessionOnRestart: whether to resume previous persisted session
6262
/// - Returns: a SnowplowSession
63-
init(foregroundTimeout: Int, backgroundTimeout: Int, trackerNamespace: String? = nil, tracker: Tracker? = nil) {
63+
init(
64+
foregroundTimeout: Int,
65+
backgroundTimeout: Int,
66+
trackerNamespace: String? = nil,
67+
tracker: Tracker? = nil,
68+
continueSessionOnRestart: Bool = false
69+
) {
6470

6571
self.foregroundTimeout = foregroundTimeout * 1000
6672
self.backgroundTimeout = backgroundTimeout * 1000
73+
self.continueSessionOnRestart = continueSessionOnRestart
74+
self.isNewSession = !continueSessionOnRestart
6775
self.tracker = tracker
6876
if let namespace = trackerNamespace {
6977
dataPersistence = DataPersistence.getFor(namespace: namespace)
@@ -73,7 +81,6 @@ class Session {
7381
if var storedSessionDict = storedSessionDict {
7482
storedSessionDict[kSPSessionUserId] = userId
7583
state = SessionState(storedState: storedSessionDict)
76-
dataPersistence?.session = storedSessionDict
7784
}
7885
if state == nil {
7986
logDiagnostic(message: "No previous session info available")
@@ -127,25 +134,27 @@ class Session {
127134
/// - firstEventTimestamp: Device created timestamp of the first event of the session
128135
/// - userAnonymisation: Whether to anonymise user identifiers
129136
/// - Returns: a SnowplowPayload containing the session dictionary
130-
func getDictWithEventId(_ eventId: String?, eventTimestamp: Int64, userAnonymisation: Bool) -> [String : Any]? {
137+
func getAndUpdateSessionForEvent(_ eventId: String?, eventTimestamp: Int64, userAnonymisation: Bool) -> [String : Any]? {
131138
var context: [String : Any]? = nil
132139

133140
if isSessionCheckerEnabled {
134-
if shouldUpdate() {
135-
update(eventId: eventId, eventTimestamp: eventTimestamp)
141+
if shouldStartNewSession() {
142+
startNewSession(eventId: eventId, eventTimestamp: eventTimestamp)
136143
if let onSessionStateUpdate = onSessionStateUpdate, let state = state {
137144
DispatchQueue.global(qos: .default).async {
138145
onSessionStateUpdate(state)
139146
}
140147
}
148+
// only persist session changes
149+
if !continueSessionOnRestart { persist() }
141150
}
142-
lastSessionCheck = Utilities.getTimestamp()
143151
}
144152

145-
eventIndex += 1
153+
state?.updateForNextEvent(isSessionCheckerEnabled: isSessionCheckerEnabled)
154+
// persist every session update
155+
if continueSessionOnRestart { persist() }
146156

147157
context = state?.sessionContext
148-
context?[kSPSessionEventIndex] = NSNumber(value: eventIndex)
149158

150159
if userAnonymisation {
151160
// mask the user identifier
@@ -180,38 +189,29 @@ class Session {
180189
return userId
181190
}
182191

183-
private func shouldUpdate() -> Bool {
192+
private func shouldStartNewSession() -> Bool {
184193
if isNewSession {
185194
return true
186195
}
187-
let lastAccess = lastSessionCheck.int64Value
188-
let now = Utilities.getTimestamp().int64Value
189-
let timeout = inBackground ? backgroundTimeout : foregroundTimeout
190-
return now < lastAccess || Int(now - lastAccess) > timeout
196+
if let state = state, let lastAccess = state.lastUpdate {
197+
let now = Utilities.getTimestamp().int64Value
198+
let timeout = inBackground ? backgroundTimeout : foregroundTimeout
199+
return now < lastAccess || Int(now - lastAccess) > timeout
200+
}
201+
return true
191202
}
192203

193-
private func update(eventId: String?, eventTimestamp: Int64) {
204+
private func startNewSession(eventId: String?, eventTimestamp: Int64) {
194205
isNewSession = false
195-
let sessionIndex = (state?.sessionIndex ?? 0) + 1
196-
let eventISOTimestamp = Utilities.timestamp(toISOString: eventTimestamp)
197-
state = SessionState(
198-
firstEventId: eventId,
199-
firstEventTimestamp: eventISOTimestamp,
200-
currentSessionId: Utilities.getUUIDString(),
201-
previousSessionId: state?.sessionId,
202-
sessionIndex: sessionIndex,
203-
userId: userId,
204-
storage: "LOCAL_STORAGE")
205-
var sessionToPersist = state?.sessionContext
206-
// Remove previousSessionId if nil because dictionaries with nil values aren't plist serializable
207-
// and can't be stored with SPDataPersistence.
208-
if state?.previousSessionId == nil {
209-
var sessionCopy = sessionToPersist
210-
sessionCopy?.removeValue(forKey: kSPSessionPreviousId)
211-
sessionToPersist = sessionCopy
206+
if let state = state {
207+
state.startNewSession(eventId: eventId, eventTimestamp: eventTimestamp)
208+
} else {
209+
state = SessionState(eventId: eventId, eventTimestamp: eventTimestamp, userId: userId)
212210
}
213-
dataPersistence?.session = sessionToPersist
214-
eventIndex = 0
211+
}
212+
213+
private func persist() {
214+
dataPersistence?.session = state?.dataToPersist
215215
}
216216

217217
// MARK: - background and foreground notifications

Sources/Core/Session/SessionControllerImpl.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ class SessionControllerImpl: Controller, SessionController {
8787
session?.backgroundTimeout = newValue * 1000
8888
}
8989
}
90+
91+
var continueSessionOnRestart: Bool {
92+
get {
93+
return session?.continueSessionOnRestart ?? TrackerDefaults.continueSessionOnRestart
94+
}
95+
set {
96+
dirtyConfig.continueSessionOnRestart = newValue
97+
session?.continueSessionOnRestart = newValue
98+
}
99+
}
100+
90101

91102
var onSessionStateUpdate: ((_ sessionState: SessionState) -> Void)? {
92103
get {

Sources/Core/Tracker/ServiceProvider.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ class ServiceProvider: NSObject, ServiceProviderProtocol {
290290
tracker.sessionContext = trackerConfiguration.sessionContext
291291
tracker.foregroundTimeout = sessionConfiguration.foregroundTimeoutInSeconds
292292
tracker.backgroundTimeout = sessionConfiguration.backgroundTimeoutInSeconds
293+
tracker.continueSessionOnRestart = sessionConfiguration.continueSessionOnRestart
293294
tracker.exceptionEvents = trackerConfiguration.exceptionAutotracking
294295
tracker.subject = subject
295296
tracker.base64Encoded = trackerConfiguration.base64Encoding

Sources/Core/Tracker/Tracker.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,20 @@ class Tracker: NSObject {
209209
}
210210
}
211211

212+
private var _continueSessionOnRestart = TrackerDefaults.continueSessionOnRestart
213+
public var continueSessionOnRestart: Bool {
214+
get {
215+
return _continueSessionOnRestart
216+
}
217+
set(continueSessionOnRestart) {
218+
_continueSessionOnRestart = continueSessionOnRestart
219+
if builderFinished && session != nil {
220+
session?.continueSessionOnRestart = continueSessionOnRestart
221+
}
222+
}
223+
}
224+
225+
212226
private var _lifecycleEvents = false
213227
/// Returns whether lifecyle events is enabled.
214228
/// - Returns: Whether background and foreground events are sent.
@@ -299,7 +313,9 @@ class Tracker: NSObject {
299313
foregroundTimeout: foregroundTimeout,
300314
backgroundTimeout: backgroundTimeout,
301315
trackerNamespace: trackerNamespace,
302-
tracker: self)
316+
tracker: self,
317+
continueSessionOnRestart: continueSessionOnRestart
318+
)
303319
}
304320

305321
if autotrackScreenViews {
@@ -588,9 +604,9 @@ class Tracker: NSObject {
588604

589605
// Add session
590606
if let session = session {
591-
if let sessionDict = session.getDictWithEventId(event.eventId.uuidString,
592-
eventTimestamp: event.timestamp,
593-
userAnonymisation: userAnonymisation) {
607+
if let sessionDict = session.getAndUpdateSessionForEvent(event.eventId.uuidString,
608+
eventTimestamp: event.timestamp,
609+
userAnonymisation: userAnonymisation) {
594610
event.addContextEntity(SelfDescribingJson(schema: kSPSessionContextSchema, andDictionary: sessionDict))
595611
} else {
596612
logDiagnostic(message: String(format: "Unable to get session context for eventId: %@", event.eventId.uuidString))

Sources/Core/Tracker/TrackerDefaults.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ class TrackerDefaults {
3333
private(set) static var geoLocationContext = false
3434
private(set) static var screenEngagementAutotracking = true
3535
private(set) static var immersiveSpaceContext = true
36+
private(set) static var continueSessionOnRestart = false
3637
}

0 commit comments

Comments
 (0)