Skip to content

Commit 5571a06

Browse files
authored
Add ability to subscribe to user state changes (#402)
* Added ability to subscribe to user state changes. * Made some logging ability public.
1 parent 34d55c8 commit 5571a06

File tree

6 files changed

+537
-216
lines changed

6 files changed

+537
-216
lines changed

.github/workflows/swift.yml

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ jobs:
1616

1717
generate_code_coverage:
1818
needs: cancel_previous
19-
runs-on: macos-15
19+
runs-on: macos-26
2020
steps:
2121
- uses: maxim-lobanov/setup-xcode@v1
2222
with:
23-
xcode-version: "16.2"
23+
xcode-version: "26"
2424
- uses: actions/checkout@v2
2525
- uses: webfactory/[email protected]
2626
with:
@@ -37,11 +37,11 @@ jobs:
3737

3838
build_and_test_spm_mac:
3939
needs: cancel_previous
40-
runs-on: macos-15
40+
runs-on: macos-26
4141
steps:
4242
- uses: maxim-lobanov/setup-xcode@v1
4343
with:
44-
xcode-version: "16.2"
44+
xcode-version: "26"
4545
- uses: actions/checkout@v2
4646
- uses: webfactory/[email protected]
4747
with:
@@ -51,24 +51,24 @@ jobs:
5151

5252
build_and_test_ios:
5353
needs: cancel_previous
54-
runs-on: macos-15
54+
runs-on: macos-26
5555
steps:
5656
- uses: maxim-lobanov/setup-xcode@v1
5757
with:
58-
xcode-version: "16.2"
58+
xcode-version: "26"
5959
- uses: actions/checkout@v2
6060
- uses: webfactory/[email protected]
6161
with:
6262
ssh-private-key: ${{ secrets.SOVRAN_SSH_KEY }}
63-
- run: xcodebuild -scheme Segment test -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16'
63+
- run: xcodebuild -scheme Segment test -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17'
6464

6565
build_and_test_tvos:
6666
needs: cancel_previous
67-
runs-on: macos-15
67+
runs-on: macos-26
6868
steps:
6969
- uses: maxim-lobanov/setup-xcode@v1
7070
with:
71-
xcode-version: "16.2"
71+
xcode-version: "26"
7272
- uses: actions/checkout@v2
7373
- uses: webfactory/[email protected]
7474
with:
@@ -77,41 +77,37 @@ jobs:
7777

7878
build_and_test_watchos:
7979
needs: cancel_previous
80-
runs-on: macos-15
80+
runs-on: macos-26
8181
steps:
8282
- uses: maxim-lobanov/setup-xcode@v1
8383
with:
84-
xcode-version: "16.2"
84+
xcode-version: "26"
8585
- uses: actions/checkout@v2
8686
- uses: webfactory/[email protected]
8787
with:
8888
ssh-private-key: ${{ secrets.SOVRAN_SSH_KEY }}
89-
- run: xcodebuild -scheme Segment test -sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Series 10 (42mm)'
89+
- run: xcodebuild -scheme Segment test -sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Ultra 3 (49mm)'
9090

9191
build_and_test_visionos:
9292
needs: cancel_previous
93-
runs-on: macos-15
93+
runs-on: macos-26
9494
steps:
9595
- uses: maxim-lobanov/setup-xcode@v1
9696
with:
97-
xcode-version: "16.2"
97+
xcode-version: "26"
9898
- uses: actions/checkout@v2
9999
- uses: webfactory/[email protected]
100100
with:
101101
ssh-private-key: ${{ secrets.SOVRAN_SSH_KEY }}
102-
- run: defaults write com.apple.dt.Xcode AllowUnsupportedVisionOSHost -bool YES
103-
- run: defaults write com.apple.CoreSimulator AllowUnsupportedVisionOSHost -bool YES
104-
- run: xcodebuild -downloadPlatform visionOS
105-
- run: echo - skip until apple fixes this - xcodebuild -scheme Segment test -sdk xrsimulator -destination 'platform=visionOS Simulator,name=Apple Vision Pro'
106-
- run: xcodebuild -scheme Segment -sdk xrsimulator -destination 'platform=visionOS Simulator,name=Apple Vision Pro'
102+
- run: xcodebuild -scheme Segment test -sdk xrsimulator -destination 'platform=visionOS Simulator,os=26,name=Apple Vision Pro'
107103

108104
build_and_test_examples:
109105
needs: cancel_previous
110-
runs-on: macos-15
106+
runs-on: macos-26
111107
steps:
112108
- uses: maxim-lobanov/setup-xcode@v1
113109
with:
114-
xcode-version: "16.2"
110+
xcode-version: "26"
115111
- uses: actions/checkout@v2
116112
- uses: webfactory/[email protected]
117113
with:

Sources/Segment/Analytics.swift

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,66 @@ extension Analytics {
292292
}
293293
}
294294
}
295-
295+
296+
/// Subscribes to UserInfo state changes.
297+
///
298+
/// The handler is called immediately with the current UserInfo, then again whenever
299+
/// the user's identity, traits, or referrer changes. The subscription remains active
300+
/// for the lifetime of the Analytics instance unless explicitly unsubscribed.
301+
///
302+
/// - Parameter handler: A closure called on the main queue with updated UserInfo.
303+
///
304+
/// - Returns: A subscription ID that can be passed to `unsubscribe(_:)` to stop
305+
/// receiving updates. If you don't need to unsubscribe, you can ignore the return value.
306+
///
307+
/// - Note: Multiple calls create multiple independent subscriptions.
308+
///
309+
/// ## Example
310+
/// ```swift
311+
/// // Subscribe for the lifetime of Analytics
312+
/// analytics.subscribeToUserInfo { userInfo in
313+
/// print("User: \(userInfo.userId ?? userInfo.anonymousId)")
314+
/// if let referrer = userInfo.referrer {
315+
/// print("Referred from: \(referrer)")
316+
/// }
317+
/// }
318+
///
319+
/// // Subscribe with manual cleanup
320+
/// let subscriptionId = analytics.subscribeToUserInfo { userInfo in
321+
/// // ... handle update
322+
/// }
323+
/// // Later, when you're done...
324+
/// analytics.unsubscribe(subscriptionId)
325+
/// ```
326+
@discardableResult
327+
public func subscribeToUserInfo(handler: @escaping (UserInfo) -> ()) -> Int {
328+
return store.subscribe(self, initialState: true, queue: .main) { (state: UserInfo) in
329+
handler(state)
330+
}
331+
}
332+
333+
/// Unsubscribes from state updates.
334+
///
335+
/// Stops receiving updates for the subscription associated with the given ID.
336+
/// After calling this, the handler will no longer be invoked for state changes.
337+
///
338+
/// - Parameter id: The subscription ID returned from a previous subscribe call.
339+
///
340+
/// - Note: Unsubscribing an already-unsubscribed or invalid ID is a no-op.
341+
///
342+
/// ## Example
343+
/// ```swift
344+
/// let id = analytics.subscribeToUserInfo { userInfo in
345+
/// print("User changed: \(userInfo.userId ?? "anonymous")")
346+
/// }
347+
///
348+
/// // Later, stop listening
349+
/// analytics.unsubscribe(id)
350+
/// ```
351+
public func unsubscribe(_ id: Int) {
352+
store.unsubscribe(identifier: id)
353+
}
354+
296355
/// Retrieve the version of this library in use.
297356
/// - Returns: A string representing the version in "BREAKING.FEATURE.FIX" format.
298357
public func version() -> String {

Sources/Segment/Settings.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public struct Settings: Codable {
1212
public var plan: JSON? = nil
1313
public var edgeFunction: JSON? = nil
1414
public var middlewareSettings: JSON? = nil
15+
public var autoInstrumentation: JSON? = nil
1516
public var metrics: JSON? = nil
1617
public var consentSettings: JSON? = nil
1718

@@ -39,6 +40,7 @@ public struct Settings: Codable {
3940
self.plan = try? values.decode(JSON.self, forKey: CodingKeys.plan)
4041
self.edgeFunction = try? values.decode(JSON.self, forKey: CodingKeys.edgeFunction)
4142
self.middlewareSettings = try? values.decode(JSON.self, forKey: CodingKeys.middlewareSettings)
43+
self.autoInstrumentation = try? values.decode(JSON.self, forKey: CodingKeys.autoInstrumentation)
4244
self.metrics = try? values.decode(JSON.self, forKey: CodingKeys.metrics)
4345
self.consentSettings = try? values.decode(JSON.self, forKey: CodingKeys.consentSettings)
4446
}
@@ -60,6 +62,7 @@ public struct Settings: Codable {
6062
case plan
6163
case edgeFunction
6264
case middlewareSettings
65+
case autoInstrumentation
6366
case metrics
6467
case consentSettings
6568
}

Sources/Segment/State.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,16 @@ struct System: State {
166166

167167
// MARK: - User information
168168

169-
struct UserInfo: Codable, State {
169+
public struct UserInfo: Codable, State {
170170
let anonymousId: String
171171
let userId: String?
172172
let traits: JSON
173173
let referrer: URL?
174174

175175
@Noncodable var anonIdGenerator: AnonymousIdGenerator?
176-
176+
}
177+
178+
extension UserInfo {
177179
struct ResetAction: Action {
178180
func reduce(state: UserInfo) -> UserInfo {
179181
var anonId: String

Sources/Segment/Utilities/Logging.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import Foundation
99

1010
extension Analytics {
11-
internal enum LogKind: CustomStringConvertible, CustomDebugStringConvertible {
11+
public enum LogKind: CustomStringConvertible, CustomDebugStringConvertible {
1212
case error
1313
case warning
1414
case debug
1515
case none
1616

17-
var description: String { return string }
18-
var debugDescription: String { return string }
17+
public var description: String { return string }
18+
public var debugDescription: String { return string }
1919

2020
var string: String {
2121
switch self {
@@ -35,7 +35,7 @@ extension Analytics {
3535
Self.segmentLog(message: message, kind: .none)
3636
}
3737

38-
static internal func segmentLog(message: String, kind: LogKind) {
38+
static public func segmentLog(message: String, kind: LogKind) {
3939
#if DEBUG
4040
if Self.debugLogsEnabled {
4141
print("\(kind)\(message)")

0 commit comments

Comments
 (0)