Skip to content

Commit ceae1e4

Browse files
authored
feat: unify several identity apis into a single Identity struct (#268)
1 parent f2510d2 commit ceae1e4

File tree

9 files changed

+476
-113
lines changed

9 files changed

+476
-113
lines changed

Amplitude-Swift.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
4E05BB942BE41AEB009DE475 /* Amplitude+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */; };
1414
4E2B646B2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */; };
1515
4E3871622BB34DBC002890AB /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B6DF481F2B5B45BE00B3E6AA /* PrivacyInfo.xcprivacy */; };
16+
4E654D682D97454900BCAA85 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E654D672D97454900BCAA85 /* Identity.swift */; };
17+
4E654DE12D9B60E700BCAA85 /* IdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E654DE02D9B60E300BCAA85 /* IdentityTests.swift */; };
1618
4EB530752CD2E46A00E961F4 /* AnalyticsConnectorFramework in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB530742CD2E46A00E961F4 /* AnalyticsConnectorFramework */; };
1719
6C04FC3F2C58973C00EA8667 /* ElementInteractionEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC3E2C58973C00EA8667 /* ElementInteractionEvent.swift */; };
1820
6C04FC412C58974A00EA8667 /* UIKitElementInteractions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C04FC402C58974A00EA8667 /* UIKitElementInteractions.swift */; };
@@ -73,7 +75,6 @@
7375
OBJ_107 /* VendorSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* VendorSystem.swift */; };
7476
OBJ_108 /* IOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* IOSLifecycleMonitor.swift */; };
7577
OBJ_109 /* WatchOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* WatchOSLifecycleMonitor.swift */; };
76-
OBJ_110 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* State.swift */; };
7778
OBJ_111 /* InMemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_38 /* InMemoryStorage.swift */; };
7879
OBJ_112 /* PersistentStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_39 /* PersistentStorage.swift */; };
7980
OBJ_113 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_40 /* Timeline.swift */; };
@@ -135,6 +136,8 @@
135136
3E281B902B9BCC14009D913B /* DispatchQueueHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueHolder.swift; sourceTree = "<group>"; };
136137
4E05BB932BE41AEB009DE475 /* Amplitude+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Amplitude+Extensions.swift"; sourceTree = "<group>"; };
137138
4E2B646A2BA127460010E6F8 /* UIKitScreenViewsPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitScreenViewsPluginTests.swift; sourceTree = "<group>"; };
139+
4E654D672D97454900BCAA85 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = "<group>"; };
140+
4E654DE02D9B60E300BCAA85 /* IdentityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityTests.swift; sourceTree = "<group>"; };
138141
6C04FC3E2C58973C00EA8667 /* ElementInteractionEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElementInteractionEvent.swift; sourceTree = "<group>"; };
139142
6C04FC402C58974A00EA8667 /* UIKitElementInteractions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitElementInteractions.swift; sourceTree = "<group>"; };
140143
6C04FC422C58976800EA8667 /* ObjCAutocaptureOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCAutocaptureOptions.swift; sourceTree = "<group>"; };
@@ -205,7 +208,6 @@
205208
OBJ_31 /* VendorSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VendorSystem.swift; sourceTree = "<group>"; };
206209
OBJ_33 /* IOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSLifecycleMonitor.swift; sourceTree = "<group>"; };
207210
OBJ_35 /* WatchOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchOSLifecycleMonitor.swift; sourceTree = "<group>"; };
208-
OBJ_36 /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
209211
OBJ_38 /* InMemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryStorage.swift; sourceTree = "<group>"; };
210212
OBJ_39 /* PersistentStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentStorage.swift; sourceTree = "<group>"; };
211213
OBJ_40 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
@@ -451,6 +453,7 @@
451453
OBJ_53 /* Tests */ = {
452454
isa = PBXGroup;
453455
children = (
456+
4E654DE02D9B60E300BCAA85 /* IdentityTests.swift */,
454457
6C04FC462C5897AC00EA8667 /* AutocaptureOptionsTests.swift */,
455458
B6F3389F2B6854A8006179E2 /* Plugins */,
456459
OBJ_54 /* AmplitudeTests.swift */,
@@ -503,6 +506,7 @@
503506
OBJ_7 /* Sources */ = {
504507
isa = PBXGroup;
505508
children = (
509+
4E654D672D97454900BCAA85 /* Identity.swift */,
506510
6C04FC442C58978900EA8667 /* AutocaptureOptions.swift */,
507511
B6DF481F2B5B45BE00B3E6AA /* PrivacyInfo.xcprivacy */,
508512
OBJ_8 /* Amplitude.swift */,
@@ -513,7 +517,6 @@
513517
OBJ_13 /* Events */,
514518
OBJ_21 /* Mediator.swift */,
515519
OBJ_22 /* Plugins */,
516-
OBJ_36 /* State.swift */,
517520
OBJ_37 /* Storages */,
518521
OBJ_40 /* Timeline.swift */,
519522
OBJ_41 /* TrackingOptions.swift */,
@@ -700,6 +703,7 @@
700703
OBJ_149 /* RevenueEventTests.swift in Sources */,
701704
OBJ_150 /* RevenueTests.swift in Sources */,
702705
OBJ_151 /* PersistentStorageTests.swift in Sources */,
706+
4E654DE12D9B60E700BCAA85 /* IdentityTests.swift in Sources */,
703707
OBJ_152 /* TestUtilities.swift in Sources */,
704708
6C23EF162C38AD31000DC8C8 /* UIKitUserInteractionPluginTest.swift in Sources */,
705709
OBJ_153 /* TimelineTests.swift in Sources */,
@@ -749,7 +753,6 @@
749753
OBJ_107 /* VendorSystem.swift in Sources */,
750754
OBJ_108 /* IOSLifecycleMonitor.swift in Sources */,
751755
OBJ_109 /* WatchOSLifecycleMonitor.swift in Sources */,
752-
OBJ_110 /* State.swift in Sources */,
753756
B6CCC6CD2B6B14510004B203 /* ObjCNetworkConnectivityCheckerPlugin.swift in Sources */,
754757
OBJ_111 /* InMemoryStorage.swift in Sources */,
755758
3E281B912B9BCC14009D913B /* DispatchQueueHolder.swift in Sources */,
@@ -785,6 +788,7 @@
785788
8EDEC51F746CC25D27E32F6A /* DeepLinkOpenedEvent.swift in Sources */,
786789
8EDECF81C2B1B38D472FD7EF /* ObjCConfiguration.swift in Sources */,
787790
8EDECB800546E37719391E65 /* ObjCPlan.swift in Sources */,
791+
4E654D682D97454900BCAA85 /* Identity.swift in Sources */,
788792
8EDEC02B99EE2092B567A61D /* ObjCIngestionMetadata.swift in Sources */,
789793
8EDEC98799DCB6610B8DBA19 /* ObjCAmplitude.swift in Sources */,
790794
8EDECD1E851511887BE000F9 /* ObjCEventOptions.swift in Sources */,

Sources/Amplitude/Amplitude.swift

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,69 @@ public class Amplitude {
88
sessions.sessionId
99
}
1010

11-
var state: State = State()
11+
private let identityLock = NSLock()
12+
private var _identity = Identity()
13+
public var identity: Identity {
14+
get {
15+
identityLock.withLock {
16+
return _identity
17+
}
18+
}
19+
set {
20+
applyIdentityUpdate(newValue)
21+
}
22+
}
23+
24+
private func applyIdentityUpdate(_ identity: Identity, sendIdentifyIfNeeded: Bool = true) {
25+
var deviceIdChanged = false
26+
var userIdChanged = false
27+
var userPropertiesChanged = false
28+
identityLock.withLock {
29+
let oldValue = _identity
30+
_identity = identity
31+
32+
if identity.deviceId != oldValue.deviceId {
33+
deviceIdChanged = true
34+
try? storage.write(key: .DEVICE_ID, value: identity.deviceId)
35+
}
36+
37+
if identity.userId != oldValue.userId {
38+
userIdChanged = true
39+
try? storage.write(key: .USER_ID, value: identity.userId)
40+
}
41+
42+
// Convert to NSDictionary to allow comparison
43+
let oldUserProperties = oldValue.userProperties as NSDictionary
44+
let newUserProperties = identity.userProperties as NSDictionary
45+
if oldUserProperties != newUserProperties {
46+
userPropertiesChanged = true
47+
}
48+
}
49+
50+
// Inform plugins after we've relinquished the lock
51+
if userIdChanged {
52+
timeline.apply { plugin in
53+
plugin.onUserIdChanged(identity.userId)
54+
}
55+
}
56+
57+
if deviceIdChanged {
58+
timeline.apply { plugin in
59+
plugin.onDeviceIdChanged(identity.deviceId)
60+
}
61+
}
62+
63+
if userIdChanged || deviceIdChanged || userPropertiesChanged {
64+
timeline.apply { plugin in
65+
plugin.onIdentityChanged(identity)
66+
}
67+
}
68+
69+
if sendIdentifyIfNeeded, userPropertiesChanged {
70+
identify(userProperties: identity.userProperties)
71+
}
72+
}
73+
1274
var contextPlugin: ContextPlugin
1375
let timeline = Timeline()
1476

@@ -56,12 +118,9 @@ public class Amplitude {
56118
}
57119
migrateInstanceOnlyStorages()
58120

59-
if let deviceId: String? = configuration.storageProvider.read(key: .DEVICE_ID) {
60-
state.deviceId = deviceId
61-
}
62-
if let userId: String? = configuration.storageProvider.read(key: .USER_ID) {
63-
state.userId = userId
64-
}
121+
_identity = Identity(userId: configuration.storageProvider.read(key: .USER_ID),
122+
deviceId: configuration.storageProvider.read(key: .DEVICE_ID),
123+
userProperties: [:])
65124

66125
if configuration.offline != NetworkConnectivityCheckerPlugin.Disabled,
67126
VendorSystem.current.networkConnectivityCheckingEnabled {
@@ -78,7 +137,9 @@ public class Amplitude {
78137

79138
// Monitor changes to optOut to send to Timeline
80139
configuration.optOutChanged = { [weak self] optOut in
81-
self?.timeline.onOptOutChanged(optOut)
140+
self?.timeline.apply {
141+
$0.onOptOutChanged(optOut)
142+
}
82143
}
83144

84145
trackingQueue.async { [self] in
@@ -131,11 +192,19 @@ public class Amplitude {
131192
event.userProperties = identify.properties as [String: Any]
132193
if let eventOptions = options {
133194
event.mergeEventOptions(eventOptions: eventOptions)
134-
if eventOptions.userId != nil {
135-
setUserId(userId: eventOptions.userId)
195+
196+
var identity = self.identity
197+
var identityChanged = false
198+
if let userId = eventOptions.userId {
199+
identityChanged = true
200+
identity.userId = userId
201+
}
202+
if let deviceId = eventOptions.deviceId {
203+
identityChanged = true
204+
identity.deviceId = deviceId
136205
}
137-
if eventOptions.deviceId != nil {
138-
setDeviceId(deviceId: eventOptions.deviceId)
206+
if identityChanged {
207+
self.identity = identity
139208
}
140209
}
141210
process(event: event)
@@ -245,21 +314,13 @@ public class Amplitude {
245314
@discardableResult
246315
public func add(plugin: Plugin) -> Amplitude {
247316
plugin.setup(amplitude: self)
248-
if let _plugin = plugin as? ObservePlugin {
249-
state.add(plugin: _plugin)
250-
} else {
251-
timeline.add(plugin: plugin)
252-
}
317+
timeline.add(plugin: plugin)
253318
return self
254319
}
255320

256321
@discardableResult
257322
public func remove(plugin: Plugin) -> Amplitude {
258-
if let _plugin = plugin as? ObservePlugin {
259-
state.remove(plugin: _plugin)
260-
} else {
261-
timeline.remove(plugin: plugin)
262-
}
323+
timeline.remove(plugin: plugin)
263324
return self
264325
}
265326

@@ -277,26 +338,22 @@ public class Amplitude {
277338

278339
@discardableResult
279340
public func setUserId(userId: String?) -> Amplitude {
280-
try? storage.write(key: .USER_ID, value: userId)
281-
state.userId = userId
282-
timeline.onUserIdChanged(userId)
341+
identity.userId = userId
283342
return self
284343
}
285344

286345
@discardableResult
287346
public func setDeviceId(deviceId: String?) -> Amplitude {
288-
try? storage.write(key: .DEVICE_ID, value: deviceId)
289-
state.deviceId = deviceId
290-
timeline.onDeviceIdChanged(deviceId)
347+
identity.deviceId = deviceId
291348
return self
292349
}
293350

294351
public func getUserId() -> String? {
295-
return state.userId
352+
return identity.userId
296353
}
297354

298355
public func getDeviceId() -> String? {
299-
return state.deviceId
356+
return identity.deviceId
300357
}
301358

302359
public func getSessionId() -> Int64 {
@@ -329,6 +386,7 @@ public class Amplitude {
329386
@discardableResult
330387
public func reset() -> Amplitude {
331388
setUserId(userId: nil)
389+
identity.userProperties.removeAll()
332390
contextPlugin.initializeDeviceId(forceReset: true)
333391
return self
334392
}
@@ -342,6 +400,17 @@ public class Amplitude {
342400
logger?.log(message: "Skip event based on opt out configuration")
343401
return
344402
}
403+
404+
if event.eventType == Constants.IDENTIFY_EVENT, let userProperties = event.userProperties {
405+
var updatedIdentity = identity
406+
updatedIdentity.apply(identify: userProperties as [String: Any])
407+
applyIdentityUpdate(updatedIdentity, sendIdentifyIfNeeded: false)
408+
}
409+
410+
let identity = self.identity
411+
event.userId = event.userId ?? identity.userId
412+
event.deviceId = event.deviceId ?? identity.deviceId
413+
345414
let inForeground = inForeground
346415
trackingQueue.async { [self] in
347416
let events = self.sessions.processEvent(event: event, inForeground: inForeground)

Sources/Amplitude/Identity.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Identity.swift
3+
// Amplitude-Swift
4+
//
5+
// Created by Chris Leonavicius on 3/28/25.
6+
//
7+
8+
public struct Identity {
9+
var userId: String?
10+
var deviceId: String?
11+
var userProperties: [String: Any] = [:]
12+
}
13+
14+
extension Identity {
15+
16+
mutating func apply(identify: [String: Any]) {
17+
var updatedProperties = userProperties
18+
19+
for property in identify {
20+
guard let op = Identify.Operation(rawValue: property.key) else {
21+
continue
22+
}
23+
24+
let opProperties = property.value as? [String: Any] ?? [:]
25+
26+
switch op {
27+
case .SET:
28+
updatedProperties.merge(opProperties) { (_, new) in new }
29+
case .CLEAR_ALL:
30+
updatedProperties = [:]
31+
case .UNSET:
32+
for (key, _) in opProperties {
33+
updatedProperties[key] = nil
34+
}
35+
case .SET_ONCE, .ADD, .APPEND, .PREPEND, .PRE_INSERT, .POST_INSERT, .REMOVE:
36+
// Unsupported
37+
break
38+
}
39+
}
40+
41+
userProperties = updatedProperties
42+
}
43+
}

Sources/Amplitude/Plugins/ContextPlugin.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ class ContextPlugin: BeforePlugin {
8989
if event.library == nil {
9090
event.library = context["library"] as? String
9191
}
92-
if event.userId == nil {
93-
event.userId = self.amplitude?.state.userId
94-
}
95-
if event.deviceId == nil {
96-
event.deviceId = self.amplitude?.state.deviceId
97-
}
9892
if event.partnerId == nil {
9993
if let pId = self.amplitude?.configuration.partnerId {
10094
event.partnerId = pId
@@ -156,7 +150,7 @@ class ContextPlugin: BeforePlugin {
156150
}
157151

158152
func initializeDeviceId(forceReset: Bool = false) {
159-
var deviceId = forceReset ? nil : amplitude?.state.deviceId
153+
var deviceId = forceReset ? nil : amplitude?.identity.deviceId
160154
if isValidDeviceId(deviceId) {
161155
return
162156
}

Sources/Amplitude/Sessions.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public class Sessions {
2121
} catch {
2222
logger?.warn(message: "Can't write PREVIOUS_SESSION_ID to storage: \(error)")
2323
}
24-
timeline.onSessionIdChanged(_sessionId)
24+
timeline.apply {
25+
$0.onSessionIdChanged(_sessionId)
26+
}
2527
}
2628
}
2729
private let sessionIdLock = NSLock()

0 commit comments

Comments
 (0)