diff --git a/ArchDiagram.png b/ArchDiagram.png index d718913..9a1794e 100644 Binary files a/ArchDiagram.png and b/ArchDiagram.png differ diff --git a/DLAnalytics.xcodeproj/project.pbxproj b/DLAnalytics.xcodeproj/project.pbxproj index a4ac6d3..167b28e 100644 --- a/DLAnalytics.xcodeproj/project.pbxproj +++ b/DLAnalytics.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 18ABFFB926FDADCC00B3EFDB /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */; }; D6C54E2E24CE0E3700BBFBF6 /* DLAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = D6C54E2C24CE0E3700BBFBF6 /* DLAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; }; D6C54E3524CE107700BBFBF6 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = D6C54E3424CE107700BBFBF6 /* README.md */; }; D6C54E3B24CE15F200BBFBF6 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C54E3A24CE15F200BBFBF6 /* AnalyticsService.swift */; }; @@ -17,6 +18,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; D6C54E2924CE0E3700BBFBF6 /* DLAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DLAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6C54E2C24CE0E3700BBFBF6 /* DLAnalytics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DLAnalytics.h; sourceTree = ""; }; D6C54E2D24CE0E3700BBFBF6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -39,6 +41,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 18ABFFB726FDADA000B3EFDB /* Extension */ = { + isa = PBXGroup; + children = ( + 18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */, + ); + path = Extension; + sourceTree = ""; + }; D6C54E1F24CE0E3700BBFBF6 = { isa = PBXGroup; children = ( @@ -59,6 +69,7 @@ D6C54E2B24CE0E3700BBFBF6 /* DLAnalytics */ = { isa = PBXGroup; children = ( + 18ABFFB726FDADA000B3EFDB /* Extension */, D6C54E3924CE15CB00BBFBF6 /* Model */, D6C54E3824CE15B600BBFBF6 /* Impl */, D6C54E3724CE159D00BBFBF6 /* Protocol */, @@ -202,6 +213,7 @@ D6C54E4124CE162300BBFBF6 /* Analytics.swift in Sources */, D6C54E3D24CE15FE00BBFBF6 /* AnalyticsEvent.swift in Sources */, D6C54E4324CE174900BBFBF6 /* ReadWriteLock.swift in Sources */, + 18ABFFB926FDADCC00B3EFDB /* Dictionary.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DLAnalytics/Extension/Dictionary.swift b/DLAnalytics/Extension/Dictionary.swift new file mode 100644 index 0000000..3ffc8f2 --- /dev/null +++ b/DLAnalytics/Extension/Dictionary.swift @@ -0,0 +1,17 @@ +// +// Dictionary.swift +// DLAnalytics +// +// Created by LeNgocDuy on 9/24/21. +// Copyright © 2021 askbills. All rights reserved. +// + +import Foundation + +public extension Dictionary { + mutating func merge(dict: [Key: Value]) { + for (key, value) in dict { + updateValue(value, forKey: key) + } + } +} diff --git a/DLAnalytics/Impl/AnalyticsManager.swift b/DLAnalytics/Impl/AnalyticsManager.swift index ca1c5bb..296748f 100644 --- a/DLAnalytics/Impl/AnalyticsManager.swift +++ b/DLAnalytics/Impl/AnalyticsManager.swift @@ -8,12 +8,15 @@ import Foundation -// MARK: - Open Class -public final class AnalyticsManager: AnalyticsService { - public static let sharedInstance = AnalyticsManager() +// MARK: - AnalyticsManager +final class AnalyticsManager { + + static var sharedInstance = AnalyticsManager() private(set) var analyticsServices: [AnalyticsService] = [] private let readWriteLock = ReadWriteLock(label: "DLAnalyticsLock") + + private var userProperty = [String: Any]() private init() {} @@ -21,22 +24,65 @@ public final class AnalyticsManager: AnalyticsService { /// /// - parameter service: An implementation of AnalyticsService. /// - returns: Void. - public func addAnalyticsService(_ service: AnalyticsService) { + func addAnalyticsService(_ service: AnalyticsService) { readWriteLock.write { analyticsServices.append(service) } } - - /// Send an Event to Analytics service. - /// This function will invoke all its childens to perform sending event. - /// - /// - returns: Void - public func send(event: AnalyticsEvent) { - var services = [AnalyticsService]() - readWriteLock.read { - services = self.analyticsServices - } - - services.forEach { $0.send(event: event) } - } + + /// To support personalization better we need to add more general properties that share with all events + func setCustomizedProperty(_ property: [String: Any]) { + userProperty.merge(dict: property) + } +} + +// MARK: - CombineEvent +public struct CombineEvent: AnalyticsEvent { + public var name: String + public var payload: [String: Any] +} + +// MARK: - AnalyticsService Implementation +extension AnalyticsManager: AnalyticsService { + /// To support identify the user we need to help set these properties as global properties + func setUserIdentifyProperty(_ property: [String: String]) { + var services = [AnalyticsService]() + readWriteLock.read { + services = self.analyticsServices + } + + services.forEach { + $0.setUserIdentifyProperty(property) + } + } + + /// Reset all data related to the user e.g user logout + func reset() { + var services = [AnalyticsService]() + readWriteLock.read { + services = self.analyticsServices + } + + services.forEach { + $0.reset() + } + } + + /// Send an Event to Analytics service. + /// This function will invoke all its childens to perform sending event. + /// + /// - returns: Void + func send(event: AnalyticsEvent) { + var services = [AnalyticsService]() + readWriteLock.read { + services = self.analyticsServices + } + + services.forEach { + var combinedPayload = userProperty + combinedPayload.merge(dict: event.payload) + let combinedEvent = CombineEvent(name: event.name, payload: combinedPayload) + $0.send(event: combinedEvent) + } + } } diff --git a/DLAnalytics/Model/Analytics.swift b/DLAnalytics/Model/Analytics.swift index 5399033..2201825 100644 --- a/DLAnalytics/Model/Analytics.swift +++ b/DLAnalytics/Model/Analytics.swift @@ -9,8 +9,28 @@ import Foundation public enum Analytics { + /// Register an imlementation of Analytics Services as consumer e.g MixPanel, Firebase, Instabug, etc.. + public static func registerAnalyticsService(_ service: AnalyticsService) { + AnalyticsManager.sharedInstance.addAnalyticsService(service) + } + /// Allow Client to send an event via. public static func send(event: AnalyticsEvent) { AnalyticsManager.sharedInstance.send(event: event) } + + /// To support personalization better we need to add more general properties that share with all events + public static func setCustomizedProperty(_ property: [String: Any]) { + AnalyticsManager.sharedInstance.setCustomizedProperty(property) + } + + /// To support identify the user we need to help set these properties as global properties + public static func setUserIdentifyProperty(_ property: [String: String]) { + AnalyticsManager.sharedInstance.setUserIdentifyProperty(property) + } + + /// Reset all data related to the user e.g user logout + public static func reset() { + AnalyticsManager.sharedInstance.reset() + } } diff --git a/DLAnalytics/Protocol/AnalyticsEvent.swift b/DLAnalytics/Protocol/AnalyticsEvent.swift index 2d2c044..2ab3f5d 100644 --- a/DLAnalytics/Protocol/AnalyticsEvent.swift +++ b/DLAnalytics/Protocol/AnalyticsEvent.swift @@ -10,5 +10,5 @@ import Foundation public protocol AnalyticsEvent { var name: String { get } - var payload: [String: String] { get } + var payload: [String: Any] { get } } diff --git a/DLAnalytics/Protocol/AnalyticsService.swift b/DLAnalytics/Protocol/AnalyticsService.swift index 15a74b7..578ce45 100644 --- a/DLAnalytics/Protocol/AnalyticsService.swift +++ b/DLAnalytics/Protocol/AnalyticsService.swift @@ -9,5 +9,12 @@ import Foundation public protocol AnalyticsService { + /// To support identify the user we need to help set these properties as global properties + func setUserIdentifyProperty(_ property: [String: String]) + + /// Reset all data related to the user e.g user logout + func reset() + + /// Send an event to Analytics func send(event: AnalyticsEvent) } diff --git a/README.md b/README.md index b0e4613..3a1dae9 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,25 @@ An abstract Analytics Framework supports: ``` class ClientAnalyticsImpl: AnalyticsService { - func send(event: AnalyticsEvent) { - print("TODO: Invoke your special API via SDK: name = \(event.name), payload = \(event.payload)") - } + func setUserIdentifyProperty(_ property: [String : String]) { + print("setUserIdentifyProperty: To support identify the user") + } + + func reset() { + print("reset: To reset all data related to the user e.g user logout") + } + + func send(event: AnalyticsEvent) { + // Here is the specific Analytics implementation e.g FireBaseAnalytics, MixPanel, etc. + print("### Send an event name: \(event.name), payload = \(event.payload)") + } } ``` 2. Declare your custom event. ``` +// MARK: - Support dynamic configurable payload for an event struct InputOTPEvent: AnalyticsEvent { private(set) var payload: [String: String] @@ -42,21 +52,32 @@ struct InputOTPEvent: AnalyticsEvent { return "InputOTP" } - static func inputOTPWrong() -> InputOTPEvent { - return InputOTPEvent(payload: ["OTPInvalid": "1"]) - } - static func inputOTPSuccess() -> InputOTPEvent { return InputOTPEvent(payload: ["OTPValid": "1"]) } } + +// MARK: - Enum support static configurable payload for an event +@frozen +enum CheckoutEvent: String, AnalyticsEvent { + case success = "Checkout_Success" + case error = "Checkout_Error" + + internal var payload: [String: Any] { + return [:] + } + + var name: String { + return rawValue + } +} ``` 3. Register your custom AnalyticsService. ``` let analyticsService = ClientAnalyticsImpl() -AnalyticsManager.sharedInstance.addAnalyticsService(analyticsService) +Analytics.registerAnalyticsService(analyticsService) ``` ### Use @@ -64,9 +85,11 @@ AnalyticsManager.sharedInstance.addAnalyticsService(analyticsService) ``` /// Simulate tracking event InputOTP success Analytics.send(event: InputOTPEvent.inputOTPSuccess()) +Analytics.send(event: CheckoutEvent.success) -/// Output: In your real implementation it is tracked on dashboard of specific Analytics (Ex: FireBaseAnalytics, MixPanel...) -TODO: Invoke your special API via SDK: name = InputOTP, payload = ["OTPValid": "1"] +/// Output: +Send an event name: InputOTP, payload = ["OTPValid": "1"] +Send an event name: Checkout_Success, payload = [:] ``` ## Installation