Skip to content

Commit 835a6f6

Browse files
feat: add identify interceptor to reduce identify volumes (#40)
* feat: add identify interceptor to reduce identify volumes * fix: shouldn't merge identify events with $clearAll, then $set * fix: linter * fix: move more logic to IdentifyInterceptor * fix: linter * fix: remove disableIdentifyBatching setting * fix: rename 'identifyUploadIntervalMillis' to 'identifyBatchIntervalMillis' * fix: pass only necessary dependencies to IdentifyInterceptor * fix: tests for QueueTimer * fix: do not force flush() on identify batch timer * fix: merge an intercepted identify event to normal event * fix: move all pipeline.put calls to IdentifyInterceptor * fix: store identify events in storage instead of single merged event * chore: linter * chore: user configuration.loggerProvider for IdentifyInterceptor * chore: remove typecast warnings in IdentifyInterceptorTests.swift * chore: lint fix * chore: WIP - refactor to limit interceptor logic to only manage intercept, merge logic * chore: WIP - refactor to limit interceptor logic to only manage intercept, merge logic * fix: flatten intercepted user properties on standard events, updated tests * chore: fix lint * chore: more lint * chore: add intercept flush test, remove dead code * chore: add intercept test using persistent storage * chore: add tests for MIN_IDENTIFY_BATCH_INTERVAL_MILLIS support * chore: lint --------- Co-authored-by: justin.fiedler <justin.fiedler@amplitude.com>
1 parent dc78228 commit 835a6f6

17 files changed

+917
-47
lines changed

Amplitude-Swift.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
/* End PBXAggregateTarget section */
2222

2323
/* Begin PBXBuildFile section */
24+
8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */; };
25+
BA9BEA4B299FB43B00BC0F7C /* IdentifyInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */; };
26+
BA9BEA4D299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */; };
2427
OBJ_100 /* Mediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* Mediator.swift */; };
2528
OBJ_101 /* AmplitudeDestinationPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* AmplitudeDestinationPlugin.swift */; };
2629
OBJ_102 /* ContextPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* ContextPlugin.swift */; };
@@ -97,6 +100,9 @@
97100
/* End PBXContainerItemProxy section */
98101

99102
/* Begin PBXFileReference section */
103+
8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueTimeTests.swift; sourceTree = "<group>"; };
104+
BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptor.swift; sourceTree = "<group>"; };
105+
BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyInterceptorTests.swift; sourceTree = "<group>"; };
100106
OBJ_10 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
101107
OBJ_11 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
102108
OBJ_12 /* EventBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBridge.swift; sourceTree = "<group>"; };
@@ -263,6 +269,7 @@
263269
OBJ_50 /* QueueTimer.swift */,
264270
OBJ_51 /* Session.swift */,
265271
OBJ_52 /* UrlExtension.swift */,
272+
BA9BEA4A299FB43B00BC0F7C /* IdentifyInterceptor.swift */,
266273
);
267274
path = Utilities;
268275
sourceTree = "<group>";
@@ -358,6 +365,8 @@
358365
OBJ_72 /* HttpClientTests.swift */,
359366
OBJ_73 /* PersistentStorageResponseHandlerTests.swift */,
360367
OBJ_74 /* UrlExtensionTests.swift */,
368+
BA9BEA4C299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift */,
369+
8EDEC4F83BFAA664749FAEF0 /* QueueTimeTests.swift */,
361370
);
362371
path = Utilities;
363372
sourceTree = "<group>";
@@ -470,6 +479,7 @@
470479
OBJ_146 /* GroupIdentifyEventTests.swift in Sources */,
471480
OBJ_147 /* IdentifyEventTests.swift in Sources */,
472481
OBJ_148 /* IdentifyTests.swift in Sources */,
482+
BA9BEA4D299FB4BB00BC0F7C /* IdentifyInterceptorTests.swift in Sources */,
473483
OBJ_149 /* RevenueEventTests.swift in Sources */,
474484
OBJ_150 /* RevenueTests.swift in Sources */,
475485
OBJ_151 /* PersistentStorageTests.swift in Sources */,
@@ -480,6 +490,7 @@
480490
OBJ_156 /* HttpClientTests.swift in Sources */,
481491
OBJ_157 /* PersistentStorageResponseHandlerTests.swift in Sources */,
482492
OBJ_158 /* UrlExtensionTests.swift in Sources */,
493+
8EDEC4EE0DE1C89889F451B5 /* QueueTimeTests.swift in Sources */,
483494
);
484495
runOnlyForDeploymentPostprocessing = 0;
485496
};
@@ -512,6 +523,7 @@
512523
OBJ_110 /* State.swift in Sources */,
513524
OBJ_111 /* InMemoryStorage.swift in Sources */,
514525
OBJ_112 /* PersistentStorage.swift in Sources */,
526+
BA9BEA4B299FB43B00BC0F7C /* IdentifyInterceptor.swift in Sources */,
515527
OBJ_113 /* Timeline.swift in Sources */,
516528
OBJ_114 /* TrackingOptions.swift in Sources */,
517529
OBJ_115 /* Types.swift in Sources */,

Sources/Amplitude/Configuration.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class Configuration {
1414
var instanceName: String
1515
var optOut: Bool
1616
var storageProvider: any Storage
17+
var identifyStorageProvider: any Storage
1718
var logLevel: LogLevelEnum
1819
var loggerProvider: any Logger
1920
var minIdLength: Int?
@@ -30,6 +31,7 @@ public class Configuration {
3031
var flushEventsOnClose: Bool?
3132
var minTimeBetweenSessionsMillis: Int
3233
var trackingSessionEvents: Bool?
34+
var identifyBatchIntervalMillis: Int
3335

3436
public init(
3537
apiKey: String,
@@ -38,6 +40,7 @@ public class Configuration {
3840
instanceName: String = Constants.Configuration.DEFAULT_INSTANCE,
3941
optOut: Bool = false,
4042
storageProvider: (any Storage)? = nil,
43+
identifyStorageProvider: (any Storage)? = nil,
4144
logLevel: LogLevelEnum = LogLevelEnum.WARN,
4245
loggerProvider: any Logger = ConsoleLogger(),
4346
minIdLength: Int? = nil,
@@ -52,16 +55,18 @@ public class Configuration {
5255
trackingOptions: TrackingOptions = TrackingOptions(),
5356
enableCoppaControl: Bool = false,
5457
flushEventsOnClose: Bool = true,
55-
minTimeBetweenSessionsMillis: Int = Constants.Configuration
56-
.MIN_TIME_BETWEEN_SESSIONS_MILLIS,
57-
trackingSessionEvents: Bool = true
58+
minTimeBetweenSessionsMillis: Int = Constants.Configuration.MIN_TIME_BETWEEN_SESSIONS_MILLIS,
59+
trackingSessionEvents: Bool = true,
60+
identifyBatchIntervalMillis: Int = Constants.Configuration.IDENTIFY_BATCH_INTERVAL_MILLIS
5861
) {
5962
self.apiKey = apiKey
6063
self.flushQueueSize = flushQueueSize
6164
self.flushIntervalMillis = flushIntervalMillis
6265
self.instanceName = instanceName
6366
self.optOut = optOut
6467
self.storageProvider = storageProvider ?? PersistentStorage(apiKey: apiKey)
68+
self.identifyStorageProvider = identifyStorageProvider
69+
?? PersistentStorage(apiKey: apiKey, storagePrefix: "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-identify")
6570
self.logLevel = logLevel
6671
self.loggerProvider = loggerProvider
6772
self.minIdLength = minIdLength
@@ -78,6 +83,7 @@ public class Configuration {
7883
self.flushEventsOnClose = flushEventsOnClose
7984
self.minTimeBetweenSessionsMillis = minTimeBetweenSessionsMillis
8085
self.trackingSessionEvents = trackingSessionEvents
86+
self.identifyBatchIntervalMillis = identifyBatchIntervalMillis
8187
// Logging is OFF by default
8288
self.loggerProvider.logLevel = logLevel.rawValue
8389
}

Sources/Amplitude/Constants.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public struct Constants {
3232
static let AMP_REVENUE_EVENT = "revenue_amount"
3333
static let MAX_PROPERTY_KEYS = 1024
3434
static let MAX_STRING_LENGTH = 1024
35+
public static let MIN_IDENTIFY_BATCH_INTERVAL_MILLIS = 30 * 1000 // 30s
3536

3637
static let AMP_TRACKING_OPTION_CARRIER = "carrier"
3738
static let AMP_TRACKING_OPTION_CITY = "city"
@@ -58,6 +59,7 @@ public struct Constants {
5859
public static let DEFAULT_INSTANCE = "default_instance"
5960
public static let FLUSH_MAX_RETRIES = 5
6061
public static let MIN_TIME_BETWEEN_SESSIONS_MILLIS = 300000
62+
public static let IDENTIFY_BATCH_INTERVAL_MILLIS = 30 * 1000 // 30s
6163
}
6264

6365
public struct Storage {

Sources/Amplitude/Events/BaseEvent.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,9 @@ extension BaseEvent {
304304
return try? decoder.decode([BaseEvent].self, from: jsonData)
305305
}
306306

307-
static func fromString(jsonString: String) -> BaseEvent? {
307+
static func fromString<T: BaseEvent>(jsonString: String) -> T? {
308308
let jsonData = jsonString.data(using: .utf8)!
309309
let decoder = JSONDecoder()
310-
return try? decoder.decode(BaseEvent.self, from: jsonData)
310+
return try? decoder.decode(T.self, from: jsonData)
311311
}
312312
}

Sources/Amplitude/Plugins/AmplitudeDestinationPlugin.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77

88
public class AmplitudeDestinationPlugin: DestinationPlugin {
99
private var pipeline: EventPipeline?
10+
private var identifyInterceptor: IdentifyInterceptor?
1011

1112
internal func enqueue(event: BaseEvent?) {
12-
if let e = event {
13-
if e.isValid() {
14-
pipeline?.put(event: e)
13+
if let event {
14+
if event.isValid() {
15+
let e = identifyInterceptor?.intercept(event: event)
16+
if let e {
17+
pipeline?.put(event: e)
18+
}
1519
} else {
1620
logger?.error(message: "Event is invalid for missing information like userId and deviceId")
1721
}
@@ -39,12 +43,17 @@ public class AmplitudeDestinationPlugin: DestinationPlugin {
3943
}
4044

4145
public override func flush() {
46+
identifyInterceptor?.transferInterceptedIdentifyEvent()
4247
pipeline?.flush()
4348
}
4449

4550
public override func setup(amplitude: Amplitude) {
4651
self.amplitude = amplitude
4752
pipeline = EventPipeline(amplitude: amplitude)
53+
identifyInterceptor = IdentifyInterceptor(
54+
configuration: amplitude.configuration,
55+
pipeline: pipeline!
56+
)
4857
pipeline?.start()
4958

5059
add(plugin: IdentityEventSender())

Sources/Amplitude/Storages/PersistentStorage.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ class PersistentStorage: Storage {
2121

2222
let syncQueue = DispatchQueue(label: "syncPersistentStorage.amplitude.com")
2323

24-
init(apiKey: String = "") {
25-
self.storagePrefix = "\(PersistentStorage.DEFAULT_STORAGE_PREFIX)-\(apiKey)"
26-
self.userDefaults = UserDefaults(suiteName: "\(PersistentStorage.AMP_STORAGE_PREFIX).\(storagePrefix)")
24+
init(apiKey: String, storagePrefix: String = PersistentStorage.DEFAULT_STORAGE_PREFIX) {
25+
self.storagePrefix = "\(storagePrefix)-\(apiKey)"
26+
self.userDefaults = UserDefaults(suiteName: "\(PersistentStorage.AMP_STORAGE_PREFIX).\(self.storagePrefix)")
2727
self.fileManager = FileManager.default
2828
self.eventCallbackMap = [String: EventCallback]()
2929
}

Sources/Amplitude/Types.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public protocol ResponseHandler {
139139
}
140140

141141
extension ResponseHandler {
142-
func collectIndices(data: [String: [Int]]) -> Set<Int> {
142+
static func collectIndices(data: [String: [Int]]) -> Set<Int> {
143143
var indices = Set<Int>()
144144
for (_, elements) in data {
145145
for el in elements {

0 commit comments

Comments
 (0)