Skip to content
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

feat: support identify user and custom properties for tracking #6

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified ArchDiagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions DLAnalytics.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -17,6 +18,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D6C54E2D24CE0E3700BBFBF6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -39,6 +41,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
18ABFFB726FDADA000B3EFDB /* Extension */ = {
isa = PBXGroup;
children = (
18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */,
);
path = Extension;
sourceTree = "<group>";
};
D6C54E1F24CE0E3700BBFBF6 = {
isa = PBXGroup;
children = (
Expand All @@ -59,6 +69,7 @@
D6C54E2B24CE0E3700BBFBF6 /* DLAnalytics */ = {
isa = PBXGroup;
children = (
18ABFFB726FDADA000B3EFDB /* Extension */,
D6C54E3924CE15CB00BBFBF6 /* Model */,
D6C54E3824CE15B600BBFBF6 /* Impl */,
D6C54E3724CE159D00BBFBF6 /* Protocol */,
Expand Down Expand Up @@ -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;
};
Expand Down
17 changes: 17 additions & 0 deletions DLAnalytics/Extension/Dictionary.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
80 changes: 63 additions & 17 deletions DLAnalytics/Impl/AnalyticsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,81 @@

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() {}

/// Add an implementation of `AnalyticsService` to a list of registered handlers.
///
/// - 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)
}
}
}
20 changes: 20 additions & 0 deletions DLAnalytics/Model/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
2 changes: 1 addition & 1 deletion DLAnalytics/Protocol/AnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ import Foundation

public protocol AnalyticsEvent {
var name: String { get }
var payload: [String: String] { get }
var payload: [String: Any] { get }
}
7 changes: 7 additions & 0 deletions DLAnalytics/Protocol/AnalyticsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,70 @@ 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]

var name: String {
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

```
/// 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
Expand Down