Skip to content

Commit d941847

Browse files
alanjcharlesbsneed
andauthored
Flush policy (#216)
* feat: add flush policy architecture * feat: add flush policy tests and interval flush policy * remove flush logic in segmentDestination and clean up tests * fix: re-add enterForeground lifecycle method * fix: move policies to project, fix memory leak * add lifecycle extensions * fix macOS and watchOS extensions --------- Co-authored-by: Alan Charles <[email protected]> Co-authored-by: Brandon Sneed <[email protected]>
1 parent 01fa6d3 commit d941847

File tree

15 files changed

+457
-64
lines changed

15 files changed

+457
-64
lines changed

Examples/apps/BasicExample/BasicExample/AppDelegate.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
1919
let configuration = Configuration(writeKey: "<WRITE KEY>")
2020
.trackApplicationLifecycleEvents(true)
2121
.flushInterval(10)
22+
.flushAt(2)
2223

2324
analytics = Analytics(configuration: configuration)
2425

Segment.xcodeproj/project.pbxproj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
46FE4CFB25A6C671003A7362 /* TestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4CFA25A6C671003A7362 /* TestUtilities.swift */; };
5555
46FE4D1D25A7A850003A7362 /* Storage_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */; };
5656
759D6CD127B48ABB00AB900A /* DestinationMetadataPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */; };
57+
823479E929F1A8280051BC99 /* FlushPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479E629F1A8280051BC99 /* FlushPolicy.swift */; };
58+
823479EA29F1A8280051BC99 /* IntervalBasedFlushPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479E729F1A8280051BC99 /* IntervalBasedFlushPolicy.swift */; };
59+
823479EB29F1A8280051BC99 /* CountBasedFlushPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479E829F1A8280051BC99 /* CountBasedFlushPolicy.swift */; };
60+
823479EE29F1A8910051BC99 /* MemoryLeak_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479EC29F1A8910051BC99 /* MemoryLeak_Tests.swift */; };
61+
823479EF29F1A8910051BC99 /* FlushPolicy_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479ED29F1A8910051BC99 /* FlushPolicy_Tests.swift */; };
5762
9620862C2575C0C800314F8D /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620862B2575C0C800314F8D /* Events.swift */; };
5863
96208650257AA83E00314F8D /* iOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */; };
5964
966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40ED259A7311008EB0B6 /* HTTPClient.swift */; };
@@ -151,6 +156,11 @@
151156
759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationMetadataPlugin.swift; sourceTree = "<group>"; };
152157
7B3C818F285BAD7600199D3E /* ComscoreDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComscoreDestination.swift; sourceTree = "<group>"; };
153158
7B3C8190285BAD8700199D3E /* IntercomDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntercomDestination.swift; sourceTree = "<group>"; };
159+
823479E629F1A8280051BC99 /* FlushPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlushPolicy.swift; sourceTree = "<group>"; };
160+
823479E729F1A8280051BC99 /* IntervalBasedFlushPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntervalBasedFlushPolicy.swift; sourceTree = "<group>"; };
161+
823479E829F1A8280051BC99 /* CountBasedFlushPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountBasedFlushPolicy.swift; sourceTree = "<group>"; };
162+
823479EC29F1A8910051BC99 /* MemoryLeak_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryLeak_Tests.swift; sourceTree = "<group>"; };
163+
823479ED29F1A8910051BC99 /* FlushPolicy_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlushPolicy_Tests.swift; sourceTree = "<group>"; };
154164
9620862B2575C0C800314F8D /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
155165
962086482579CCC200314F8D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
156166
9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLifecycleMonitor.swift; sourceTree = "<group>"; };
@@ -274,6 +284,16 @@
274284
path = Examples/other_plugins;
275285
sourceTree = "<group>";
276286
};
287+
823479E529F1A8280051BC99 /* Policies */ = {
288+
isa = PBXGroup;
289+
children = (
290+
823479E629F1A8280051BC99 /* FlushPolicy.swift */,
291+
823479E729F1A8280051BC99 /* IntervalBasedFlushPolicy.swift */,
292+
823479E829F1A8280051BC99 /* CountBasedFlushPolicy.swift */,
293+
);
294+
path = Policies;
295+
sourceTree = "<group>";
296+
};
277297
96208624256DC23F00314F8D /* Frameworks */ = {
278298
isa = PBXGroup;
279299
children = (
@@ -337,6 +357,7 @@
337357
A31A16A325780A8D00C9CDDF /* Utilities */ = {
338358
isa = PBXGroup;
339359
children = (
360+
823479E529F1A8280051BC99 /* Policies */,
340361
460FF30A29BA525900635FF9 /* Logging.swift */,
341362
967C40ED259A7311008EB0B6 /* HTTPClient.swift */,
342363
A31A16A425780E8D00C9CDDF /* JSON.swift */,
@@ -362,6 +383,8 @@
362383
OBJ_11 /* Segment-Tests */ = {
363384
isa = PBXGroup;
364385
children = (
386+
823479ED29F1A8910051BC99 /* FlushPolicy_Tests.swift */,
387+
823479EC29F1A8910051BC99 /* MemoryLeak_Tests.swift */,
365388
OBJ_12 /* Analytics_Tests.swift */,
366389
4658175325BA4C20006B2809 /* HTTPClient_Tests.swift */,
367390
A31A16502576C47400C9CDDF /* JSON_Tests.swift */,
@@ -537,6 +560,7 @@
537560
46E382E72654429A00BA2502 /* Utils.swift in Sources */,
538561
A31A16B225781CB400C9CDDF /* JSON.swift in Sources */,
539562
46022771261F7A4800A9E913 /* Atomic.swift in Sources */,
563+
823479EB29F1A8280051BC99 /* CountBasedFlushPolicy.swift in Sources */,
540564
46F7485D26C718710042798E /* ObjCAnalytics.swift in Sources */,
541565
A3471FBE256487F000965480 /* Configuration.swift in Sources */,
542566
OBJ_23 /* Analytics.swift in Sources */,
@@ -545,6 +569,7 @@
545569
4689231429F7391500AB26E5 /* ObjCEvents.swift in Sources */,
546570
A31A16E12579779600C9CDDF /* Version.swift in Sources */,
547571
46210836260BBEE400EBC4A8 /* DeviceToken.swift in Sources */,
572+
823479E929F1A8280051BC99 /* FlushPolicy.swift in Sources */,
548573
9692724E25A4E5B7009B5298 /* Startup.swift in Sources */,
549574
4663C729267A799100ADDD1A /* QueueTimer.swift in Sources */,
550575
4689231329F7391500AB26E5 /* ObjCPlugin.swift in Sources */,
@@ -559,6 +584,7 @@
559584
466EC2CE28FB7D5D001B384E /* OutputFileStream.swift in Sources */,
560585
46FE4C9725A3F35E003A7362 /* macOSLifecycleMonitor.swift in Sources */,
561586
9620862C2575C0C800314F8D /* Events.swift in Sources */,
587+
823479EA29F1A8280051BC99 /* IntervalBasedFlushPolicy.swift in Sources */,
562588
A3AEE1882581A8F1002386EB /* Deprecations.swift in Sources */,
563589
966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */,
564590
A31A16CA25794D9700C9CDDF /* Plugins.swift in Sources */,
@@ -578,7 +604,9 @@
578604
OBJ_30 /* Analytics_Tests.swift in Sources */,
579605
46F7486026C720F60042798E /* ObjC_Tests.swift in Sources */,
580606
OBJ_31 /* XCTestManifests.swift in Sources */,
607+
823479EF29F1A8910051BC99 /* FlushPolicy_Tests.swift in Sources */,
581608
46B1AC6927346D3D00846DE8 /* StressTests.swift in Sources */,
609+
823479EE29F1A8910051BC99 /* MemoryLeak_Tests.swift in Sources */,
582610
4658175425BA4C20006B2809 /* HTTPClient_Tests.swift in Sources */,
583611
46210811260538BE00EBC4A8 /* KeyPath_Tests.swift in Sources */,
584612
96A9668927BC137F00078F8B /* iOSLifecycle_Tests.swift in Sources */,

Sources/Segment/Analytics.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,18 @@ public class Analytics {
4747
internal func process<E: RawEvent>(incomingEvent: E) {
4848
guard enabled == true else { return }
4949
let event = incomingEvent.applyRawEventData(store: store)
50+
5051
_ = timeline.process(incomingEvent: event)
52+
53+
let flushPolicies = configuration.values.flushPolicies
54+
for policy in flushPolicies {
55+
policy.updateState(event: event)
56+
57+
if (policy.shouldFlush() == true) {
58+
flush()
59+
policy.reset()
60+
}
61+
}
5162
}
5263

5364
/// Process a raw event through the system. Useful when one needs to queue and replay events at a later time.
@@ -131,6 +142,13 @@ extension Analytics {
131142
}
132143
}
133144

145+
public var flushPolicies: [FlushPolicy] {
146+
147+
get {
148+
configuration.values.flushPolicies
149+
}
150+
}
151+
134152
/// Returns the traits that were specified in the last identify call.
135153
public func traits<T: Codable>() -> T? {
136154
if let userInfo: UserInfo = store.currentState() {

Sources/Segment/Configuration.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class Configuration {
2525
var cdnHost: String = HTTPClient.getDefaultCDNHost()
2626
var requestFactory: ((URLRequest) -> URLRequest)? = nil
2727
var errorHandler: ((Error) -> Void)? = nil
28+
var flushPolicies: [FlushPolicy] = [CountBasedFlushPolicy(), IntervalBasedFlushPolicy()]
2829
}
2930

3031
internal var values: Values
@@ -175,6 +176,12 @@ public extension Configuration {
175176
values.errorHandler = value
176177
return self
177178
}
179+
180+
@discardableResult
181+
func flushPolicies(_ policies: [FlushPolicy]) -> Configuration {
182+
values.flushPolicies = policies
183+
return self
184+
}
178185
}
179186

180187
extension Analytics {

Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,16 @@ extension SegmentDestination: macOSLifecycle {
239239
}
240240
}
241241

242+
// MARK: - Interval Based Flush Policy Extension
243+
244+
extension IntervalBasedFlushPolicy: macOSLifecycle {
245+
public func applicationWillEnterForeground() {
246+
enterForeground()
247+
}
248+
249+
public func applicationDidEnterBackground() {
250+
enterBackground()
251+
}
252+
}
253+
242254
#endif

Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class iOSLifecycleMonitor: PlatformPlugin {
174174

175175
// MARK: - Segment Destination Extension
176176

177+
177178
extension SegmentDestination: iOSLifecycle {
178179
public func applicationWillEnterForeground(application: UIApplication?) {
179180
enterForeground()
@@ -203,6 +204,18 @@ extension SegmentDestination.UploadTaskInfo {
203204
}
204205
}
205206

207+
// MARK: - Interval Based Flush Policy Extension
208+
209+
extension IntervalBasedFlushPolicy: iOSLifecycle {
210+
public func applicationWillEnterForeground(application: UIApplication?) {
211+
enterForeground()
212+
}
213+
214+
public func applicationDidEnterBackground(application: UIApplication?) {
215+
enterBackground()
216+
}
217+
}
218+
206219
extension UIApplication {
207220
static var safeShared: UIApplication? {
208221
// UIApplication.shared is not available in app extensions so try to get

Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,16 @@ extension SegmentDestination: watchOSLifecycle {
131131
}
132132
}
133133

134+
135+
// MARK: - Interval Based Flush Policy Extension
136+
137+
extension IntervalBasedFlushPolicy: watchOSLifecycle {
138+
public func applicationWillEnterForeground(watchExtension: WKExtension) {
139+
enterForeground()
140+
}
141+
142+
public func applicationDidEnterBackground(watchExtension: WKExtension) {
143+
enterBackground()
144+
}
145+
}
134146
#endif

Sources/Segment/Plugins/SegmentDestination.swift

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,12 @@ public class SegmentDestination: DestinationPlugin, Subscriber {
4646
private var storage: Storage?
4747

4848
@Atomic internal var eventCount: Int = 0
49-
internal var flushAt: Int = 0
50-
internal var flushTimer: QueueTimer? = nil
5149

5250
internal func initialSetup() {
5351
guard let analytics = self.analytics else { return }
5452
storage = analytics.storage
5553
httpClient = HTTPClient(analytics: analytics)
5654

57-
// flushInterval and flushAt can be modified post initialization
58-
analytics.store.subscribe(self, initialState: true) { [weak self] (state: System) in
59-
guard let self = self else { return }
60-
self.flushTimer = QueueTimer(interval: state.configuration.values.flushInterval) { [weak self] in
61-
self?.flush()
62-
}
63-
self.flushAt = state.configuration.values.flushAt
64-
}
65-
6655
// Add DestinationMetadata enrichment plugin
6756
add(plugin: DestinationMetadataPlugin())
6857
}
@@ -109,25 +98,18 @@ public class SegmentDestination: DestinationPlugin, Subscriber {
10998
}
11099

111100
// MARK: - Abstracted Lifecycle Methods
112-
internal func enterForeground() {
113-
flushTimer?.resume()
114-
}
101+
internal func enterForeground() { }
115102

116103
internal func enterBackground() {
117-
flushTimer?.suspend()
118104
flush()
119105
}
120106

121107
// MARK: - Event Parsing Methods
122108
private func queueEvent<T: RawEvent>(event: T) {
123109
guard let storage = self.storage else { return }
124-
125110
// Send Event to File System
126111
storage.write(.events, value: event)
127112
eventCount += 1
128-
if eventCount >= flushAt {
129-
flush()
130-
}
131113
}
132114

133115
public func flush() {

Sources/Segment/Startup.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ extension Analytics: Subscriber {
2828
}
2929
}
3030

31+
for policy in configuration.values.flushPolicies {
32+
policy.configure(analytics: self)
33+
}
34+
3135
// plugins will receive any settings we currently have as they are added.
3236
// ... but lets go check if we have new stuff ....
3337
// start checking periodically for settings changes from segment.com
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// CountBasedFlushPolicy.swift
3+
//
4+
//
5+
// Created by Alan Charles on 3/21/23.
6+
//
7+
8+
import Foundation
9+
10+
public class CountBasedFlushPolicy: FlushPolicy {
11+
public weak var analytics: Analytics?
12+
internal var desiredCount: Int?
13+
internal var count: Int = 0
14+
15+
init() { }
16+
17+
init(count: Int) {
18+
desiredCount = count
19+
}
20+
21+
public func configure(analytics: Analytics) {
22+
self.analytics = analytics
23+
if let desiredCount = desiredCount {
24+
analytics.flushAt = desiredCount
25+
}
26+
}
27+
28+
public func shouldFlush() -> Bool {
29+
guard let a = analytics else {
30+
return false
31+
}
32+
if a.configuration.values.flushAt > 0 && count >= a.configuration.values.flushAt {
33+
return true
34+
} else {
35+
return false
36+
}
37+
}
38+
39+
public func updateState(event: RawEvent) {
40+
count += 1
41+
}
42+
43+
public func reset() {
44+
count = 0
45+
}
46+
}

0 commit comments

Comments
 (0)