Skip to content

Commit e67e944

Browse files
authored
feat: support identify user and custom properties for tracking (#6)
Support identify user, custom properties to help tracking more personalized data BREAKING CHANGE: `Client` only interact with the framework via `Analytics` instead of mix of `Analytics` and `AnalyticsManager` as previous versions
1 parent 82589d2 commit e67e944

File tree

8 files changed

+153
-28
lines changed

8 files changed

+153
-28
lines changed

ArchDiagram.png

90.8 KB
Loading

DLAnalytics.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
18ABFFB926FDADCC00B3EFDB /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */; };
1011
D6C54E2E24CE0E3700BBFBF6 /* DLAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = D6C54E2C24CE0E3700BBFBF6 /* DLAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; };
1112
D6C54E3524CE107700BBFBF6 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = D6C54E3424CE107700BBFBF6 /* README.md */; };
1213
D6C54E3B24CE15F200BBFBF6 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C54E3A24CE15F200BBFBF6 /* AnalyticsService.swift */; };
@@ -17,6 +18,7 @@
1718
/* End PBXBuildFile section */
1819

1920
/* Begin PBXFileReference section */
21+
18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
2022
D6C54E2924CE0E3700BBFBF6 /* DLAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DLAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2123
D6C54E2C24CE0E3700BBFBF6 /* DLAnalytics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DLAnalytics.h; sourceTree = "<group>"; };
2224
D6C54E2D24CE0E3700BBFBF6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -39,6 +41,14 @@
3941
/* End PBXFrameworksBuildPhase section */
4042

4143
/* Begin PBXGroup section */
44+
18ABFFB726FDADA000B3EFDB /* Extension */ = {
45+
isa = PBXGroup;
46+
children = (
47+
18ABFFB826FDADCC00B3EFDB /* Dictionary.swift */,
48+
);
49+
path = Extension;
50+
sourceTree = "<group>";
51+
};
4252
D6C54E1F24CE0E3700BBFBF6 = {
4353
isa = PBXGroup;
4454
children = (
@@ -59,6 +69,7 @@
5969
D6C54E2B24CE0E3700BBFBF6 /* DLAnalytics */ = {
6070
isa = PBXGroup;
6171
children = (
72+
18ABFFB726FDADA000B3EFDB /* Extension */,
6273
D6C54E3924CE15CB00BBFBF6 /* Model */,
6374
D6C54E3824CE15B600BBFBF6 /* Impl */,
6475
D6C54E3724CE159D00BBFBF6 /* Protocol */,
@@ -202,6 +213,7 @@
202213
D6C54E4124CE162300BBFBF6 /* Analytics.swift in Sources */,
203214
D6C54E3D24CE15FE00BBFBF6 /* AnalyticsEvent.swift in Sources */,
204215
D6C54E4324CE174900BBFBF6 /* ReadWriteLock.swift in Sources */,
216+
18ABFFB926FDADCC00B3EFDB /* Dictionary.swift in Sources */,
205217
);
206218
runOnlyForDeploymentPostprocessing = 0;
207219
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// Dictionary.swift
3+
// DLAnalytics
4+
//
5+
// Created by LeNgocDuy on 9/24/21.
6+
// Copyright © 2021 askbills. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public extension Dictionary {
12+
mutating func merge(dict: [Key: Value]) {
13+
for (key, value) in dict {
14+
updateValue(value, forKey: key)
15+
}
16+
}
17+
}

DLAnalytics/Impl/AnalyticsManager.swift

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,81 @@
88

99
import Foundation
1010

11-
// MARK: - Open Class
12-
public final class AnalyticsManager: AnalyticsService {
13-
public static let sharedInstance = AnalyticsManager()
11+
// MARK: - AnalyticsManager
12+
final class AnalyticsManager {
13+
14+
static var sharedInstance = AnalyticsManager()
1415

1516
private(set) var analyticsServices: [AnalyticsService] = []
1617
private let readWriteLock = ReadWriteLock(label: "DLAnalyticsLock")
18+
19+
private var userProperty = [String: Any]()
1720

1821
private init() {}
1922

2023
/// Add an implementation of `AnalyticsService` to a list of registered handlers.
2124
///
2225
/// - parameter service: An implementation of AnalyticsService.
2326
/// - returns: Void.
24-
public func addAnalyticsService(_ service: AnalyticsService) {
27+
func addAnalyticsService(_ service: AnalyticsService) {
2528
readWriteLock.write {
2629
analyticsServices.append(service)
2730
}
2831
}
29-
30-
/// Send an Event to Analytics service.
31-
/// This function will invoke all its childens to perform sending event.
32-
///
33-
/// - returns: Void
34-
public func send(event: AnalyticsEvent) {
35-
var services = [AnalyticsService]()
36-
readWriteLock.read {
37-
services = self.analyticsServices
38-
}
39-
40-
services.forEach { $0.send(event: event) }
41-
}
32+
33+
/// To support personalization better we need to add more general properties that share with all events
34+
func setCustomizedProperty(_ property: [String: Any]) {
35+
userProperty.merge(dict: property)
36+
}
37+
}
38+
39+
// MARK: - CombineEvent
40+
public struct CombineEvent: AnalyticsEvent {
41+
public var name: String
42+
public var payload: [String: Any]
43+
}
44+
45+
// MARK: - AnalyticsService Implementation
46+
extension AnalyticsManager: AnalyticsService {
47+
/// To support identify the user we need to help set these properties as global properties
48+
func setUserIdentifyProperty(_ property: [String: String]) {
49+
var services = [AnalyticsService]()
50+
readWriteLock.read {
51+
services = self.analyticsServices
52+
}
53+
54+
services.forEach {
55+
$0.setUserIdentifyProperty(property)
56+
}
57+
}
58+
59+
/// Reset all data related to the user e.g user logout
60+
func reset() {
61+
var services = [AnalyticsService]()
62+
readWriteLock.read {
63+
services = self.analyticsServices
64+
}
65+
66+
services.forEach {
67+
$0.reset()
68+
}
69+
}
70+
71+
/// Send an Event to Analytics service.
72+
/// This function will invoke all its childens to perform sending event.
73+
///
74+
/// - returns: Void
75+
func send(event: AnalyticsEvent) {
76+
var services = [AnalyticsService]()
77+
readWriteLock.read {
78+
services = self.analyticsServices
79+
}
80+
81+
services.forEach {
82+
var combinedPayload = userProperty
83+
combinedPayload.merge(dict: event.payload)
84+
let combinedEvent = CombineEvent(name: event.name, payload: combinedPayload)
85+
$0.send(event: combinedEvent)
86+
}
87+
}
4288
}

DLAnalytics/Model/Analytics.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,28 @@
99
import Foundation
1010

1111
public enum Analytics {
12+
/// Register an imlementation of Analytics Services as consumer e.g MixPanel, Firebase, Instabug, etc..
13+
public static func registerAnalyticsService(_ service: AnalyticsService) {
14+
AnalyticsManager.sharedInstance.addAnalyticsService(service)
15+
}
16+
1217
/// Allow Client to send an event via.
1318
public static func send(event: AnalyticsEvent) {
1419
AnalyticsManager.sharedInstance.send(event: event)
1520
}
21+
22+
/// To support personalization better we need to add more general properties that share with all events
23+
public static func setCustomizedProperty(_ property: [String: Any]) {
24+
AnalyticsManager.sharedInstance.setCustomizedProperty(property)
25+
}
26+
27+
/// To support identify the user we need to help set these properties as global properties
28+
public static func setUserIdentifyProperty(_ property: [String: String]) {
29+
AnalyticsManager.sharedInstance.setUserIdentifyProperty(property)
30+
}
31+
32+
/// Reset all data related to the user e.g user logout
33+
public static func reset() {
34+
AnalyticsManager.sharedInstance.reset()
35+
}
1636
}

DLAnalytics/Protocol/AnalyticsEvent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ import Foundation
1010

1111
public protocol AnalyticsEvent {
1212
var name: String { get }
13-
var payload: [String: String] { get }
13+
var payload: [String: Any] { get }
1414
}

DLAnalytics/Protocol/AnalyticsService.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,12 @@
99
import Foundation
1010

1111
public protocol AnalyticsService {
12+
/// To support identify the user we need to help set these properties as global properties
13+
func setUserIdentifyProperty(_ property: [String: String])
14+
15+
/// Reset all data related to the user e.g user logout
16+
func reset()
17+
18+
/// Send an event to Analytics
1219
func send(event: AnalyticsEvent)
1320
}

README.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,47 +26,70 @@ An abstract Analytics Framework supports:
2626

2727
```
2828
class ClientAnalyticsImpl: AnalyticsService {
29-
func send(event: AnalyticsEvent) {
30-
print("TODO: Invoke your special API via SDK: name = \(event.name), payload = \(event.payload)")
31-
}
29+
func setUserIdentifyProperty(_ property: [String : String]) {
30+
print("setUserIdentifyProperty: To support identify the user")
31+
}
32+
33+
func reset() {
34+
print("reset: To reset all data related to the user e.g user logout")
35+
}
36+
37+
func send(event: AnalyticsEvent) {
38+
// Here is the specific Analytics implementation e.g FireBaseAnalytics, MixPanel, etc.
39+
print("### Send an event name: \(event.name), payload = \(event.payload)")
40+
}
3241
}
3342
```
3443

3544
2. Declare your custom event.
3645

3746
```
47+
// MARK: - Support dynamic configurable payload for an event
3848
struct InputOTPEvent: AnalyticsEvent {
3949
private(set) var payload: [String: String]
4050
4151
var name: String {
4252
return "InputOTP"
4353
}
4454
45-
static func inputOTPWrong() -> InputOTPEvent {
46-
return InputOTPEvent(payload: ["OTPInvalid": "1"])
47-
}
48-
4955
static func inputOTPSuccess() -> InputOTPEvent {
5056
return InputOTPEvent(payload: ["OTPValid": "1"])
5157
}
5258
}
59+
60+
// MARK: - Enum support static configurable payload for an event
61+
@frozen
62+
enum CheckoutEvent: String, AnalyticsEvent {
63+
case success = "Checkout_Success"
64+
case error = "Checkout_Error"
65+
66+
internal var payload: [String: Any] {
67+
return [:]
68+
}
69+
70+
var name: String {
71+
return rawValue
72+
}
73+
}
5374
```
5475

5576
3. Register your custom AnalyticsService.
5677

5778
```
5879
let analyticsService = ClientAnalyticsImpl()
59-
AnalyticsManager.sharedInstance.addAnalyticsService(analyticsService)
80+
Analytics.registerAnalyticsService(analyticsService)
6081
```
6182

6283
### Use
6384

6485
```
6586
/// Simulate tracking event InputOTP success
6687
Analytics.send(event: InputOTPEvent.inputOTPSuccess())
88+
Analytics.send(event: CheckoutEvent.success)
6789
68-
/// Output: In your real implementation it is tracked on dashboard of specific Analytics (Ex: FireBaseAnalytics, MixPanel...)
69-
TODO: Invoke your special API via SDK: name = InputOTP, payload = ["OTPValid": "1"]
90+
/// Output:
91+
Send an event name: InputOTP, payload = ["OTPValid": "1"]
92+
Send an event name: Checkout_Success, payload = [:]
7093
```
7194

7295
## Installation

0 commit comments

Comments
 (0)