Skip to content

Flush policy #216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 26, 2023
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