-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Manage Events - track app lifecycle events (#118)
* move all the logic inside confidence * add evaluation extension in flag resolution * cleanup * have the evaluation in the confidence module for the flags * update demo app to support only confidence * fix some tests to move to confidence * use int instead of int64 for 32bits system to work with default value for int, fix some more tests * fixup! use int instead of int64 for 32bits system to work with default value for int, fix some more tests * fixup! Merge branch 'main' into move-flag-evaluation-confidence * fixup! fixup! Merge branch 'main' into move-flag-evaluation-confidence * fixup! fixup! fixup! Merge branch 'main' into move-flag-evaluation-confidence * add analytics for app and ui kit lifecycle, add the appear and disappear for demo app events * fixup! add analytics for app and ui kit lifecycle, add the appear and disappear for demo app events * handle app launch and app install and app updates * add context producer and produce context for is_foreground and build and version * fixup! merge main * fixup! fixup! merge main * refactor: Smaller refactor and fixes * Remove some managed events * wip: minor fixes and experiments * introduce passthrough subject with buffer * update the engine to remove the batches by default and add them if failed * fixup! update the engine to remove the batches by default and add them if failed * queue label prefix * use semaphore to make upload serialz * Remove dead code * Make buffered passthrough serial --------- Co-authored-by: Fabrizio Demaria <[email protected]>
- Loading branch information
1 parent
bfdc949
commit e74af7c
Showing
9 changed files
with
334 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Foundation | ||
import Combine | ||
|
||
class BufferedPassthrough<T> { | ||
private let subject = PassthroughSubject<T, Never>() | ||
private var buffer: [T] = [] | ||
private var isListening = false | ||
private let queue = DispatchQueue(label: "com.confidence.passthrough_serial") | ||
|
||
func send(_ value: T) { | ||
queue.sync { | ||
if isListening { | ||
subject.send(value) | ||
} else { | ||
buffer.append(value) | ||
} | ||
} | ||
} | ||
|
||
func publisher() -> AnyPublisher<T, Never> { | ||
return queue.sync { | ||
isListening = true | ||
let bufferedPublisher = buffer.publisher | ||
buffer.removeAll() | ||
return bufferedPublisher | ||
.append(subject) | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
Sources/Confidence/ConfidenceAppLifecycleProducer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) | ||
import Foundation | ||
import UIKit | ||
import Combine | ||
|
||
public class ConfidenceAppLifecycleProducer: ConfidenceEventProducer, ConfidenceContextProducer, ObservableObject { | ||
public var currentProducedContext: CurrentValueSubject<ConfidenceStruct, Never> = CurrentValueSubject([:]) | ||
private var events: BufferedPassthrough<Event> = BufferedPassthrough() | ||
private let queue = DispatchQueue(label: "com.confidence.lifecycle_producer") | ||
private var appNotifications: [NSNotification.Name] = [ | ||
UIApplication.didEnterBackgroundNotification, | ||
UIApplication.willEnterForegroundNotification, | ||
UIApplication.didBecomeActiveNotification | ||
] | ||
|
||
private static var versionNameKey = "CONFIDENCE_VERSION_NAME_KEY" | ||
private static var buildNameKey = "CONFIDENCE_VERSIONN_KEY" | ||
private let appLaunchedEventName = "app-launched" | ||
|
||
public init() { | ||
for notification in appNotifications { | ||
NotificationCenter.default.addObserver( | ||
self, | ||
selector: #selector(notificationResponse), | ||
name: notification, | ||
object: nil | ||
) | ||
} | ||
|
||
let currentVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" | ||
let currentBuild: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" | ||
let context: ConfidenceStruct = [ | ||
"version": .init(string: currentVersion), | ||
"build": .init(string: currentBuild) | ||
] | ||
|
||
self.currentProducedContext.send(context) | ||
} | ||
|
||
deinit { | ||
NotificationCenter.default.removeObserver(self) | ||
} | ||
|
||
public func produceEvents() -> AnyPublisher<Event, Never> { | ||
events.publisher() | ||
} | ||
|
||
public func produceContexts() -> AnyPublisher<ConfidenceStruct, Never> { | ||
currentProducedContext | ||
.filter { context in !context.isEmpty } | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
private func track(eventName: String) { | ||
let previousBuild: String? = UserDefaults.standard.string(forKey: Self.buildNameKey) | ||
let previousVersion: String? = UserDefaults.standard.string(forKey: Self.versionNameKey) | ||
|
||
let currentVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" | ||
let currentBuild: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" | ||
|
||
let message: ConfidenceStruct = [ | ||
"version": .init(string: currentVersion), | ||
"build": .init(string: currentBuild) | ||
] | ||
|
||
if eventName == self.appLaunchedEventName { | ||
if previousBuild == nil && previousVersion == nil { | ||
events.send(Event(name: "app-installed", message: message)) | ||
} else if previousBuild != currentBuild || previousVersion != currentVersion { | ||
events.send(Event(name: "app-updated", message: message)) | ||
} | ||
} | ||
events.send(Event(name: eventName, message: message)) | ||
|
||
UserDefaults.standard.setValue(currentVersion, forKey: Self.versionNameKey) | ||
UserDefaults.standard.setValue(currentBuild, forKey: Self.buildNameKey) | ||
} | ||
|
||
private func updateContext(isForeground: Bool) { | ||
withLock { [weak self] in | ||
guard let self = self else { | ||
return | ||
} | ||
var context = self.currentProducedContext.value | ||
context.updateValue(.init(boolean: isForeground), forKey: "is_foreground") | ||
self.currentProducedContext.send(context) | ||
} | ||
} | ||
|
||
private func withLock(callback: @escaping () -> Void) { | ||
queue.sync { | ||
callback() | ||
} | ||
} | ||
|
||
@objc func notificationResponse(notification: NSNotification) { | ||
switch notification.name { | ||
case UIApplication.didEnterBackgroundNotification: | ||
updateContext(isForeground: false) | ||
case UIApplication.willEnterForegroundNotification: | ||
updateContext(isForeground: true) | ||
case UIApplication.didBecomeActiveNotification: | ||
track(eventName: appLaunchedEventName) | ||
default: | ||
break | ||
} | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import Foundation | ||
import Combine | ||
|
||
/** | ||
ConfidenceContextProducer or ConfidenceEventProducer | ||
*/ | ||
public protocol ConfidenceProducer { | ||
} | ||
|
||
public struct Event { | ||
let name: String | ||
let message: ConfidenceStruct | ||
|
||
public init(name: String, message: ConfidenceStruct = [:]) { | ||
self.name = name | ||
self.message = message | ||
} | ||
} | ||
|
||
/** | ||
ConfidenceContextProducer implementer pushses context changes in a Publisher fashion | ||
*/ | ||
public protocol ConfidenceContextProducer: ConfidenceProducer { | ||
func produceContexts() -> AnyPublisher<ConfidenceStruct, Never> | ||
} | ||
|
||
/** | ||
ConfidenceContextProducer implementer emit events in a Publisher fashion | ||
*/ | ||
public protocol ConfidenceEventProducer: ConfidenceProducer { | ||
func produceEvents() -> AnyPublisher<Event, Never> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) | ||
import Foundation | ||
import UIKit | ||
import Combine | ||
|
||
public class ConfidenceScreenTracker: ConfidenceEventProducer { | ||
private var events = BufferedPassthrough<Event>() | ||
static let notificationName = Notification.Name(rawValue: "ConfidenceScreenTracker") | ||
static let screenName = "screen_name" | ||
static let messageKey = "message_json" | ||
static let controllerKey = "controller" | ||
|
||
public init() { | ||
swizzle( | ||
forClass: UIViewController.self, | ||
original: #selector(UIViewController.viewDidAppear(_:)), | ||
new: #selector(UIViewController.confidence__viewDidAppear) | ||
) | ||
|
||
swizzle( | ||
forClass: UIViewController.self, | ||
original: #selector(UIViewController.viewDidDisappear(_:)), | ||
new: #selector(UIViewController.confidence__viewDidDisappear) | ||
) | ||
|
||
NotificationCenter.default.addObserver( | ||
forName: Self.notificationName, | ||
object: nil, | ||
queue: OperationQueue.main) { [weak self] notification in | ||
let name = notification.userInfo?[Self.screenName] as? String | ||
let messageJson = (notification.userInfo?[Self.messageKey] as? String)?.data(using: .utf8) | ||
var message: ConfidenceStruct = [:] | ||
if let data = messageJson { | ||
let decoder = JSONDecoder() | ||
do { | ||
message = try decoder.decode(ConfidenceStruct.self, from: data) | ||
} catch { | ||
} | ||
} | ||
|
||
guard let self = self else { | ||
return | ||
} | ||
if let name = name { | ||
self.events.send(Event(name: name, message: message)) | ||
} | ||
} | ||
} | ||
|
||
public func produceEvents() -> AnyPublisher<Event, Never> { | ||
events.publisher() | ||
} | ||
|
||
private func swizzle(forClass: AnyClass, original: Selector, new: Selector) { | ||
guard let originalMethod = class_getInstanceMethod(forClass, original) else { return } | ||
guard let swizzledMethod = class_getInstanceMethod(forClass, new) else { return } | ||
method_exchangeImplementations(originalMethod, swizzledMethod) | ||
} | ||
} | ||
|
||
public protocol TrackableComponent { | ||
func trackName() -> String | ||
} | ||
|
||
public protocol TrackableComponentWithMessage: TrackableComponent { | ||
func trackMessage() -> ConfidenceStruct | ||
} | ||
|
||
extension UIViewController { | ||
private func sendNotification(event: String) { | ||
var className = String(describing: type(of: self)) | ||
.replacingOccurrences(of: "ViewController", with: "") | ||
var message: [String: String] = [ConfidenceScreenTracker.screenName: className] | ||
|
||
if let trackable = self as? TrackableComponent { | ||
className = trackable.trackName() | ||
if let trackableWithMessage = self as? TrackableComponentWithMessage { | ||
let encoder = JSONEncoder() | ||
do { | ||
let data = try encoder.encode(trackableWithMessage.trackMessage()) | ||
let messageString = String(data: data, encoding: .utf8) | ||
if let json = messageString { | ||
message.updateValue(json, forKey: ConfidenceScreenTracker.messageKey) | ||
} | ||
} catch { | ||
} | ||
} | ||
} | ||
|
||
NotificationCenter.default.post( | ||
name: ConfidenceScreenTracker.notificationName, | ||
object: self, | ||
userInfo: message | ||
) | ||
} | ||
@objc internal func confidence__viewDidAppear(animated: Bool) { | ||
} | ||
|
||
@objc internal func confidence__viewDidDisappear(animated: Bool) { | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.