Skip to content

Commit 6b53ebf

Browse files
[V3] Add Networking to Send Analytics (#75)
* Change FPTIBatchData structure * Add NetworkClient and AnalyticsServices files * Add Sessionable and NetworkError files * Add Network errors * Add Sessionable Protocol and Extension * Sort files * Add NetworkClient * Implement AnalyticsService class * Add Private Mark * Add necessary files (TestPlan, mock files and UT files) * Setup Test Plan * Create MockSession class and NonEncodable structure * Add NetworkClient tests * Create MockNetworkClient class * Sort files * Add missing blank space * Add UTs to test success and failures * Remove iOS 15 validation * Add analytics calls * Set sessionID value * Add AnalyticsService property * Add MockAnalyticsService * Fix PopupBridge tests adding mock * Increase time interval * Add MockAnalyticsService * Add PopupBrdige UTs * Strongly type FPTIBatchData * Add catch block * Update Sources/PopupBridge/FPTIBatchData.swift Co-authored-by: Jax DesMarais-Leder <jdesmarais@paypal.com> * Add analytics folder * Remove test plan * Address feedback * Sort files --------- Co-authored-by: Jax DesMarais-Leder <jdesmarais@paypal.com>
1 parent 4ed21f3 commit 6b53ebf

14 files changed

+443
-101
lines changed

Demo/UITests/PopupBridge_DemoUITests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ final class PopupBridge_DemoUITests: XCTestCase {
5656

5757
// MARK: - Helpers
5858

59-
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 10) {
59+
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 15) {
6060
expectation(for: NSPredicate(format: "exists ==1"), evaluatedWith: element)
6161
waitForExpectations(timeout: timeout)
6262
}

PopupBridge.xcodeproj/project.pbxproj

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
45AE41D22D7649F800388548 /* MockAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */; };
1011
45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */; };
1112
45CD0C2E2D664D140072C5A4 /* Date+MilisecondTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */; };
1213
45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */; };
1314
45CD0C322D6793FB0072C5A4 /* PopupBridgeAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */; };
15+
45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; };
16+
45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF282D701AF7000D550B /* Sessionable.swift */; };
17+
45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2A2D701B0D000D550B /* NetworkError.swift */; };
18+
45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */; };
19+
45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */; };
1420
45FC74C22D8347B500E50035 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */; };
1521
62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */; };
1622
79DB9F7F53319F206CDE119E /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */; };
@@ -31,10 +37,16 @@
3137

3238
/* Begin PBXFileReference section */
3339
28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
40+
45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalyticsService.swift; sourceTree = "<group>"; };
3441
45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = "<group>"; };
3542
45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+MilisecondTimestamp.swift"; sourceTree = "<group>"; };
3643
45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = "<group>"; };
3744
45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupBridgeAnalytics.swift; sourceTree = "<group>"; };
45+
45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
46+
45FBAF282D701AF7000D550B /* Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sessionable.swift; sourceTree = "<group>"; };
47+
45FBAF2A2D701B0D000D550B /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
48+
45FBAF2E2D701E1D000D550B /* MockSessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionable.swift; sourceTree = "<group>"; };
49+
45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService_Test.swift; sourceTree = "<group>"; };
3850
45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+URLOpener.swift"; sourceTree = "<group>"; };
3951
4EF7C7DDAB0B99FF28DD6541 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = "<group>"; };
4052
6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
@@ -96,6 +108,16 @@
96108
path = Pods;
97109
sourceTree = "<group>";
98110
};
111+
45FC74C32D84922F00E50035 /* Analytics */ = {
112+
isa = PBXGroup;
113+
children = (
114+
45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */,
115+
45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */,
116+
45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */,
117+
);
118+
path = Analytics;
119+
sourceTree = "<group>";
120+
};
99121
6003F581195388D10070C39A = {
100122
isa = PBXGroup;
101123
children = (
@@ -153,8 +175,11 @@
153175
A775A07D1DEE4E7E009E67C2 /* UnitTests */ = {
154176
isa = PBXGroup;
155177
children = (
178+
45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */,
156179
A775A0801DEE4E7E009E67C2 /* Info.plist */,
180+
45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */,
157181
BE2524552A17FB9F00168D77 /* MockScriptMessage.swift */,
182+
45FBAF2E2D701E1D000D550B /* MockSessionable.swift */,
158183
BE2524532A17DFCC00168D77 /* MockUserContentController.swift */,
159184
BEF9ED202A2A4896005D54AB /* MockWebAuthenticationSession.swift */,
160185
BE2524512A17DF8200168D77 /* PopupBridge_UnitTests.swift */,
@@ -165,16 +190,17 @@
165190
A775A08C1DEE4EF0009E67C2 /* PopupBridge */ = {
166191
isa = PBXGroup;
167192
children = (
168-
45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */,
193+
45FC74C32D84922F00E50035 /* Analytics */,
169194
45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */,
170195
45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */,
171-
45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */,
196+
45FBAF2A2D701B0D000D550B /* NetworkError.swift */,
172197
800A09D72995F143003ED16E /* POPPopupBridge.swift */,
173198
A79330F01DF0F98F00EE479D /* PopupBridge-Framework-Info.plist */,
174-
45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */,
175199
BEF9ED222A2A6A2C005D54AB /* PopupBridgeConstants.swift */,
176200
8079D7192996F6C200A2E336 /* PopupBridgeUserScript.swift */,
177201
62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */,
202+
45FBAF282D701AF7000D550B /* Sessionable.swift */,
203+
45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */,
178204
800E789E29E09A2A00D1B0FC /* URLDetailsPayload.swift */,
179205
BE8E37B52A17B79E00181FDA /* WebAuthenticationSession.swift */,
180206
800E789C29E0958A00D1B0FC /* WebViewMessage.swift */,
@@ -320,19 +346,25 @@
320346
isa = PBXSourcesBuildPhase;
321347
buildActionMask = 2147483647;
322348
files = (
349+
45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */,
323350
BE2524542A17DFCC00168D77 /* MockUserContentController.swift in Sources */,
351+
45AE41D22D7649F800388548 /* MockAnalyticsService.swift in Sources */,
324352
BE2524562A17FB9F00168D77 /* MockScriptMessage.swift in Sources */,
325353
BEF9ED212A2A4896005D54AB /* MockWebAuthenticationSession.swift in Sources */,
326354
BE2524522A17DF8200168D77 /* PopupBridge_UnitTests.swift in Sources */,
355+
45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */,
327356
);
328357
runOnlyForDeploymentPostprocessing = 0;
329358
};
330359
A775A0861DEE4EF0009E67C2 /* Sources */ = {
331360
isa = PBXSourcesBuildPhase;
332361
buildActionMask = 2147483647;
333362
files = (
363+
45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */,
364+
45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */,
334365
800E789F29E09A2A00D1B0FC /* URLDetailsPayload.swift in Sources */,
335366
45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */,
367+
45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */,
336368
BEF9ED232A2A6A2C005D54AB /* PopupBridgeConstants.swift in Sources */,
337369
45FC74C22D8347B500E50035 /* UIApplication+URLOpener.swift in Sources */,
338370
800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Foundation
2+
3+
protocol AnalyticsServiceable {
4+
func sendAnalyticsEvent(_ eventName: String, sessionID: String)
5+
}
6+
7+
final class AnalyticsService: AnalyticsServiceable {
8+
9+
// MARK: - Private Properties
10+
11+
/// The FPTI URL to post all analytic events.
12+
private let url = URL(string: "https://api.paypal.com/v1/tracking/batch/events")!
13+
private let session: Sessionable
14+
15+
// MARK: - Initializer
16+
17+
init(session: Sessionable = URLSession.shared) {
18+
self.session = session
19+
}
20+
21+
// MARK: - Internal Methods
22+
23+
func sendAnalyticsEvent(_ eventName: String, sessionID: String) {
24+
Task(priority: .background) {
25+
await performEventRequest(eventName, sessionID: sessionID)
26+
}
27+
}
28+
29+
func performEventRequest(_ eventName: String, sessionID: String) async {
30+
let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID)
31+
do {
32+
try await post(url: url, body: body)
33+
} catch {
34+
NSLog("[PopupBridge SDK] Failed to send analytics: %@", error.localizedDescription)
35+
}
36+
}
37+
38+
// MARK: - Private Methods
39+
40+
/// Constructs POST params to be sent to FPTI
41+
private func createAnalyticsEvent(eventName: String, sessionID: String) -> FPTIBatchData {
42+
let batchMetadata = FPTIBatchData.Metadata(sessionID: sessionID)
43+
let event = FPTIBatchData.Event(eventName: eventName)
44+
return FPTIBatchData(metadata: batchMetadata, events: [event])
45+
}
46+
47+
private func post(url: URL, body: FPTIBatchData) async throws {
48+
var request = URLRequest(url: url)
49+
request.httpMethod = "POST"
50+
request.allHTTPHeaderFields = ["Content-Type": "application/json"]
51+
52+
let encodedBody = try JSONEncoder().encode(body)
53+
request.httpBody = encodedBody
54+
55+
let (_, response) = try await session.data(for: request)
56+
57+
guard let httpResponse = response as? HTTPURLResponse,
58+
(200...299).contains(httpResponse.statusCode) else {
59+
throw NetworkError.invalidResponse
60+
}
61+
}
62+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import UIKit
2+
3+
struct FPTIBatchData: Codable {
4+
5+
let events: [EventsContainer]
6+
7+
init(metadata: Metadata, events fptiEvents: [Event]) {
8+
self.events = [
9+
EventsContainer(
10+
metadata: metadata,
11+
fptiEvents: fptiEvents
12+
)
13+
]
14+
}
15+
16+
struct EventsContainer: Codable {
17+
18+
let metadata: Metadata
19+
let fptiEvents: [Event]
20+
21+
enum CodingKeys: String, CodingKey {
22+
case metadata = "batch_params"
23+
case fptiEvents = "event_params"
24+
}
25+
}
26+
27+
/// Encapsulates a single event by it's name and timestamp.
28+
struct Event: Codable {
29+
30+
let eventName: String
31+
32+
let timestamp: String = String(Date().utcTimestampMilliseconds)
33+
34+
let tenantName: String = "Braintree"
35+
36+
enum CodingKeys: String, CodingKey {
37+
case eventName = "event_name"
38+
case timestamp = "t"
39+
case tenantName = "tenant_name"
40+
}
41+
}
42+
43+
/// The FPTI tags/metadata applicable to all events in the batch upload.
44+
struct Metadata: Codable {
45+
46+
let appID: String = Bundle.main.infoDictionary?[kCFBundleIdentifierKey as String] as? String ?? "N/A"
47+
48+
let appName: String = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "N/A"
49+
50+
let clientSDKVersion: String = Bundle.clientSDKVersion
51+
52+
let clientOS: String = UIDevice.current.systemName + " " + UIDevice.current.systemVersion
53+
54+
let component: String = "popupbridgesdk"
55+
56+
let deviceManufacturer: String = "Apple"
57+
58+
let deviceModel: String = {
59+
var systemInfo = utsname()
60+
uname(&systemInfo)
61+
let machineMirror = Mirror(reflecting: systemInfo.machine)
62+
let identifier = machineMirror.children.reduce("") { identifier, element in
63+
guard let value = element.value as? Int8, value != 0 else { return identifier }
64+
return identifier + String(UnicodeScalar(UInt8(value)))
65+
}
66+
return identifier
67+
}()
68+
69+
let eventSource: String = "mobile-native"
70+
71+
let isSimulator: Bool = {
72+
#if targetEnvironment(simulator)
73+
true
74+
#else
75+
false
76+
#endif
77+
}()
78+
79+
let merchantAppVersion: String = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? "N/A"
80+
81+
let packageManager: String = {
82+
#if COCOAPODS
83+
"CocoaPods"
84+
#elseif SWIFT_PACKAGE
85+
"Swift Package Manager"
86+
#else
87+
"Carthage or Other"
88+
#endif
89+
}()
90+
91+
let platform: String = "iOS"
92+
93+
let sessionID: String
94+
95+
enum CodingKeys: String, CodingKey {
96+
case appID = "app_id"
97+
case appName = "app_name"
98+
case clientSDKVersion = "c_sdk_ver"
99+
case clientOS = "client_os"
100+
case component = "comp"
101+
case deviceManufacturer = "device_manufacturer"
102+
case deviceModel = "mobile_device_model"
103+
case eventSource = "event_source"
104+
case isSimulator = "is_simulator"
105+
case merchantAppVersion = "mapv"
106+
case packageManager = "ios_package_manager"
107+
case platform
108+
case sessionID = "session_id"
109+
}
110+
}
111+
}
File renamed without changes.

Sources/PopupBridge/FPTIBatchData.swift

Lines changed: 0 additions & 78 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
enum NetworkError: Error {
4+
case invalidResponse
5+
}

0 commit comments

Comments
 (0)