Skip to content

Commit

Permalink
Flush policy (#216)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Apr 26, 2023
1 parent 01fa6d3 commit d941847
Show file tree
Hide file tree
Showing 15 changed files with 457 additions and 64 deletions.
1 change: 1 addition & 0 deletions Examples/apps/BasicExample/BasicExample/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let configuration = Configuration(writeKey: "<WRITE KEY>")
.trackApplicationLifecycleEvents(true)
.flushInterval(10)
.flushAt(2)

analytics = Analytics(configuration: configuration)

Expand Down
28 changes: 28 additions & 0 deletions Segment.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
46FE4CFB25A6C671003A7362 /* TestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4CFA25A6C671003A7362 /* TestUtilities.swift */; };
46FE4D1D25A7A850003A7362 /* Storage_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */; };
759D6CD127B48ABB00AB900A /* DestinationMetadataPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */; };
823479E929F1A8280051BC99 /* FlushPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479E629F1A8280051BC99 /* FlushPolicy.swift */; };
823479EA29F1A8280051BC99 /* IntervalBasedFlushPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479E729F1A8280051BC99 /* IntervalBasedFlushPolicy.swift */; };
823479EB29F1A8280051BC99 /* CountBasedFlushPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479E829F1A8280051BC99 /* CountBasedFlushPolicy.swift */; };
823479EE29F1A8910051BC99 /* MemoryLeak_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479EC29F1A8910051BC99 /* MemoryLeak_Tests.swift */; };
823479EF29F1A8910051BC99 /* FlushPolicy_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823479ED29F1A8910051BC99 /* FlushPolicy_Tests.swift */; };
9620862C2575C0C800314F8D /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620862B2575C0C800314F8D /* Events.swift */; };
96208650257AA83E00314F8D /* iOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */; };
966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40ED259A7311008EB0B6 /* HTTPClient.swift */; };
Expand Down Expand Up @@ -151,6 +156,11 @@
759D6CD027B48ABB00AB900A /* DestinationMetadataPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationMetadataPlugin.swift; sourceTree = "<group>"; };
7B3C818F285BAD7600199D3E /* ComscoreDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComscoreDestination.swift; sourceTree = "<group>"; };
7B3C8190285BAD8700199D3E /* IntercomDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntercomDestination.swift; sourceTree = "<group>"; };
823479E629F1A8280051BC99 /* FlushPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlushPolicy.swift; sourceTree = "<group>"; };
823479E729F1A8280051BC99 /* IntervalBasedFlushPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntervalBasedFlushPolicy.swift; sourceTree = "<group>"; };
823479E829F1A8280051BC99 /* CountBasedFlushPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountBasedFlushPolicy.swift; sourceTree = "<group>"; };
823479EC29F1A8910051BC99 /* MemoryLeak_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryLeak_Tests.swift; sourceTree = "<group>"; };
823479ED29F1A8910051BC99 /* FlushPolicy_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlushPolicy_Tests.swift; sourceTree = "<group>"; };
9620862B2575C0C800314F8D /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
962086482579CCC200314F8D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLifecycleMonitor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -274,6 +284,16 @@
path = Examples/other_plugins;
sourceTree = "<group>";
};
823479E529F1A8280051BC99 /* Policies */ = {
isa = PBXGroup;
children = (
823479E629F1A8280051BC99 /* FlushPolicy.swift */,
823479E729F1A8280051BC99 /* IntervalBasedFlushPolicy.swift */,
823479E829F1A8280051BC99 /* CountBasedFlushPolicy.swift */,
);
path = Policies;
sourceTree = "<group>";
};
96208624256DC23F00314F8D /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -337,6 +357,7 @@
A31A16A325780A8D00C9CDDF /* Utilities */ = {
isa = PBXGroup;
children = (
823479E529F1A8280051BC99 /* Policies */,
460FF30A29BA525900635FF9 /* Logging.swift */,
967C40ED259A7311008EB0B6 /* HTTPClient.swift */,
A31A16A425780E8D00C9CDDF /* JSON.swift */,
Expand All @@ -362,6 +383,8 @@
OBJ_11 /* Segment-Tests */ = {
isa = PBXGroup;
children = (
823479ED29F1A8910051BC99 /* FlushPolicy_Tests.swift */,
823479EC29F1A8910051BC99 /* MemoryLeak_Tests.swift */,
OBJ_12 /* Analytics_Tests.swift */,
4658175325BA4C20006B2809 /* HTTPClient_Tests.swift */,
A31A16502576C47400C9CDDF /* JSON_Tests.swift */,
Expand Down Expand Up @@ -537,6 +560,7 @@
46E382E72654429A00BA2502 /* Utils.swift in Sources */,
A31A16B225781CB400C9CDDF /* JSON.swift in Sources */,
46022771261F7A4800A9E913 /* Atomic.swift in Sources */,
823479EB29F1A8280051BC99 /* CountBasedFlushPolicy.swift in Sources */,
46F7485D26C718710042798E /* ObjCAnalytics.swift in Sources */,
A3471FBE256487F000965480 /* Configuration.swift in Sources */,
OBJ_23 /* Analytics.swift in Sources */,
Expand All @@ -545,6 +569,7 @@
4689231429F7391500AB26E5 /* ObjCEvents.swift in Sources */,
A31A16E12579779600C9CDDF /* Version.swift in Sources */,
46210836260BBEE400EBC4A8 /* DeviceToken.swift in Sources */,
823479E929F1A8280051BC99 /* FlushPolicy.swift in Sources */,
9692724E25A4E5B7009B5298 /* Startup.swift in Sources */,
4663C729267A799100ADDD1A /* QueueTimer.swift in Sources */,
4689231329F7391500AB26E5 /* ObjCPlugin.swift in Sources */,
Expand All @@ -559,6 +584,7 @@
466EC2CE28FB7D5D001B384E /* OutputFileStream.swift in Sources */,
46FE4C9725A3F35E003A7362 /* macOSLifecycleMonitor.swift in Sources */,
9620862C2575C0C800314F8D /* Events.swift in Sources */,
823479EA29F1A8280051BC99 /* IntervalBasedFlushPolicy.swift in Sources */,
A3AEE1882581A8F1002386EB /* Deprecations.swift in Sources */,
966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */,
A31A16CA25794D9700C9CDDF /* Plugins.swift in Sources */,
Expand All @@ -578,7 +604,9 @@
OBJ_30 /* Analytics_Tests.swift in Sources */,
46F7486026C720F60042798E /* ObjC_Tests.swift in Sources */,
OBJ_31 /* XCTestManifests.swift in Sources */,
823479EF29F1A8910051BC99 /* FlushPolicy_Tests.swift in Sources */,
46B1AC6927346D3D00846DE8 /* StressTests.swift in Sources */,
823479EE29F1A8910051BC99 /* MemoryLeak_Tests.swift in Sources */,
4658175425BA4C20006B2809 /* HTTPClient_Tests.swift in Sources */,
46210811260538BE00EBC4A8 /* KeyPath_Tests.swift in Sources */,
96A9668927BC137F00078F8B /* iOSLifecycle_Tests.swift in Sources */,
Expand Down
18 changes: 18 additions & 0 deletions Sources/Segment/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ public class Analytics {
internal func process<E: RawEvent>(incomingEvent: E) {
guard enabled == true else { return }
let event = incomingEvent.applyRawEventData(store: store)

_ = timeline.process(incomingEvent: event)

let flushPolicies = configuration.values.flushPolicies
for policy in flushPolicies {
policy.updateState(event: event)

if (policy.shouldFlush() == true) {
flush()
policy.reset()
}
}
}

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

public var flushPolicies: [FlushPolicy] {

get {
configuration.values.flushPolicies
}
}

/// Returns the traits that were specified in the last identify call.
public func traits<T: Codable>() -> T? {
if let userInfo: UserInfo = store.currentState() {
Expand Down
7 changes: 7 additions & 0 deletions Sources/Segment/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class Configuration {
var cdnHost: String = HTTPClient.getDefaultCDNHost()
var requestFactory: ((URLRequest) -> URLRequest)? = nil
var errorHandler: ((Error) -> Void)? = nil
var flushPolicies: [FlushPolicy] = [CountBasedFlushPolicy(), IntervalBasedFlushPolicy()]
}

internal var values: Values
Expand Down Expand Up @@ -175,6 +176,12 @@ public extension Configuration {
values.errorHandler = value
return self
}

@discardableResult
func flushPolicies(_ policies: [FlushPolicy]) -> Configuration {
values.flushPolicies = policies
return self
}
}

extension Analytics {
Expand Down
12 changes: 12 additions & 0 deletions Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,16 @@ extension SegmentDestination: macOSLifecycle {
}
}

// MARK: - Interval Based Flush Policy Extension

extension IntervalBasedFlushPolicy: macOSLifecycle {
public func applicationWillEnterForeground() {
enterForeground()
}

public func applicationDidEnterBackground() {
enterBackground()
}
}

#endif
13 changes: 13 additions & 0 deletions Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ class iOSLifecycleMonitor: PlatformPlugin {

// MARK: - Segment Destination Extension


extension SegmentDestination: iOSLifecycle {
public func applicationWillEnterForeground(application: UIApplication?) {
enterForeground()
Expand Down Expand Up @@ -203,6 +204,18 @@ extension SegmentDestination.UploadTaskInfo {
}
}

// MARK: - Interval Based Flush Policy Extension

extension IntervalBasedFlushPolicy: iOSLifecycle {
public func applicationWillEnterForeground(application: UIApplication?) {
enterForeground()
}

public func applicationDidEnterBackground(application: UIApplication?) {
enterBackground()
}
}

extension UIApplication {
static var safeShared: UIApplication? {
// UIApplication.shared is not available in app extensions so try to get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,16 @@ extension SegmentDestination: watchOSLifecycle {
}
}


// MARK: - Interval Based Flush Policy Extension

extension IntervalBasedFlushPolicy: watchOSLifecycle {
public func applicationWillEnterForeground(watchExtension: WKExtension) {
enterForeground()
}

public func applicationDidEnterBackground(watchExtension: WKExtension) {
enterBackground()
}
}
#endif
20 changes: 1 addition & 19 deletions Sources/Segment/Plugins/SegmentDestination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,12 @@ public class SegmentDestination: DestinationPlugin, Subscriber {
private var storage: Storage?

@Atomic internal var eventCount: Int = 0
internal var flushAt: Int = 0
internal var flushTimer: QueueTimer? = nil

internal func initialSetup() {
guard let analytics = self.analytics else { return }
storage = analytics.storage
httpClient = HTTPClient(analytics: analytics)

// flushInterval and flushAt can be modified post initialization
analytics.store.subscribe(self, initialState: true) { [weak self] (state: System) in
guard let self = self else { return }
self.flushTimer = QueueTimer(interval: state.configuration.values.flushInterval) { [weak self] in
self?.flush()
}
self.flushAt = state.configuration.values.flushAt
}

// Add DestinationMetadata enrichment plugin
add(plugin: DestinationMetadataPlugin())
}
Expand Down Expand Up @@ -109,25 +98,18 @@ public class SegmentDestination: DestinationPlugin, Subscriber {
}

// MARK: - Abstracted Lifecycle Methods
internal func enterForeground() {
flushTimer?.resume()
}
internal func enterForeground() { }

internal func enterBackground() {
flushTimer?.suspend()
flush()
}

// MARK: - Event Parsing Methods
private func queueEvent<T: RawEvent>(event: T) {
guard let storage = self.storage else { return }

// Send Event to File System
storage.write(.events, value: event)
eventCount += 1
if eventCount >= flushAt {
flush()
}
}

public func flush() {
Expand Down
4 changes: 4 additions & 0 deletions Sources/Segment/Startup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ extension Analytics: Subscriber {
}
}

for policy in configuration.values.flushPolicies {
policy.configure(analytics: self)
}

// plugins will receive any settings we currently have as they are added.
// ... but lets go check if we have new stuff ....
// start checking periodically for settings changes from segment.com
Expand Down
46 changes: 46 additions & 0 deletions Sources/Segment/Utilities/Policies/CountBasedFlushPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// CountBasedFlushPolicy.swift
//
//
// Created by Alan Charles on 3/21/23.
//

import Foundation

public class CountBasedFlushPolicy: FlushPolicy {
public weak var analytics: Analytics?
internal var desiredCount: Int?
internal var count: Int = 0

init() { }

init(count: Int) {
desiredCount = count
}

public func configure(analytics: Analytics) {
self.analytics = analytics
if let desiredCount = desiredCount {
analytics.flushAt = desiredCount
}
}

public func shouldFlush() -> Bool {
guard let a = analytics else {
return false
}
if a.configuration.values.flushAt > 0 && count >= a.configuration.values.flushAt {
return true
} else {
return false
}
}

public func updateState(event: RawEvent) {
count += 1
}

public func reset() {
count = 0
}
}
65 changes: 65 additions & 0 deletions Sources/Segment/Utilities/Policies/FlushPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// FlushPolicy.swift
//
//
// Created by Alan Charles on 3/21/23.
//

import Foundation

public protocol FlushPolicy: AnyObject {
var analytics: Analytics? { get set }
func configure(analytics: Analytics) -> Void
func shouldFlush() -> Bool
func updateState(event: RawEvent) -> Void
func reset() -> Void
}

public extension Analytics {
func add(flushPolicy: FlushPolicy) {
guard let state: System = store.currentState() else { return }
let config = state.configuration
var policies = config.values.flushPolicies
policies.append(flushPolicy)
config.flushPolicies(policies)
store.dispatch(action: System.UpdateConfigurationAction(configuration: config))

flushPolicy.configure(analytics: self)
}

func remove(flushPolicy: FlushPolicy) {
guard let state: System = store.currentState() else { return }
let config = state.configuration
let policies = config.values.flushPolicies.filter { policy in
return flushPolicy !== policy
}
config.flushPolicies(policies)
store.dispatch(action: System.UpdateConfigurationAction(configuration: config))
}

func remove<T: FlushPolicy>(flushPolicy: T.Type) {
guard let state: System = store.currentState() else { return }
let config = state.configuration
let policies = config.values.flushPolicies.filter { policy in
return !(policy is T)
}
config.flushPolicies(policies)
store.dispatch(action: System.UpdateConfigurationAction(configuration: config))
}

func removeAllFlushPolicies() {
guard let state: System = store.currentState() else { return }
let config = state.configuration
config.flushPolicies([])
store.dispatch(action: System.UpdateConfigurationAction(configuration: config))
}

func find<T: FlushPolicy>(flushPolicy: T.Type) -> FlushPolicy? {
guard let state: System = store.currentState() else { return nil }
let config = state.configuration
let found = config.values.flushPolicies.filter { policy in
return policy is T
}
return found.first
}
}
Loading

0 comments on commit d941847

Please sign in to comment.