-
Notifications
You must be signed in to change notification settings - Fork 21
[V3] Add Networking to Send Analytics #75
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
Changes from all commits
d272d2c
304190d
03f9365
52fd81c
6224055
4a4e97f
4f87b20
0d71ffb
060e7e4
ddb944c
37ea28a
05cf278
6310175
b03bdac
d6fc4fc
022530d
c9e846c
292affb
5308717
a268cd0
9734fb3
3ba4d21
700f695
fd33535
d0a8d8d
147dc9b
9a5e980
a658633
70bb98e
c8fd4cf
5911546
7bc9147
39a61a6
ec82e7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import Foundation | ||
|
||
protocol AnalyticsServiceable { | ||
func sendAnalyticsEvent(_ eventName: String, sessionID: String) | ||
} | ||
|
||
final class AnalyticsService: AnalyticsServiceable { | ||
|
||
// MARK: - Private Properties | ||
|
||
/// The FPTI URL to post all analytic events. | ||
private let url = URL(string: "https://api.paypal.com/v1/tracking/batch/events")! | ||
private let session: Sessionable | ||
|
||
// MARK: - Initializer | ||
|
||
init(session: Sessionable = URLSession.shared) { | ||
self.session = session | ||
} | ||
|
||
// MARK: - Internal Methods | ||
|
||
func sendAnalyticsEvent(_ eventName: String, sessionID: String) { | ||
Task(priority: .background) { | ||
await performEventRequest(eventName, sessionID: sessionID) | ||
} | ||
} | ||
|
||
func performEventRequest(_ eventName: String, sessionID: String) async { | ||
let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID) | ||
do { | ||
try await post(url: url, body: body) | ||
} catch { | ||
NSLog("[PopupBridge SDK] Failed to send analytics: %@", error.localizedDescription) | ||
} | ||
} | ||
|
||
// MARK: - Private Methods | ||
|
||
/// Constructs POST params to be sent to FPTI | ||
private func createAnalyticsEvent(eventName: String, sessionID: String) -> FPTIBatchData { | ||
let batchMetadata = FPTIBatchData.Metadata(sessionID: sessionID) | ||
let event = FPTIBatchData.Event(eventName: eventName) | ||
return FPTIBatchData(metadata: batchMetadata, events: [event]) | ||
} | ||
|
||
private func post(url: URL, body: FPTIBatchData) async throws { | ||
var request = URLRequest(url: url) | ||
request.httpMethod = "POST" | ||
request.allHTTPHeaderFields = ["Content-Type": "application/json"] | ||
|
||
let encodedBody = try JSONEncoder().encode(body) | ||
request.httpBody = encodedBody | ||
|
||
let (_, response) = try await session.data(for: request) | ||
|
||
guard let httpResponse = response as? HTTPURLResponse, | ||
(200...299).contains(httpResponse.statusCode) else { | ||
throw NetworkError.invalidResponse | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import UIKit | ||
|
||
struct FPTIBatchData: Codable { | ||
|
||
let events: [EventsContainer] | ||
|
||
init(metadata: Metadata, events fptiEvents: [Event]) { | ||
self.events = [ | ||
EventsContainer( | ||
metadata: metadata, | ||
fptiEvents: fptiEvents | ||
) | ||
] | ||
} | ||
|
||
struct EventsContainer: Codable { | ||
|
||
let metadata: Metadata | ||
let fptiEvents: [Event] | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case metadata = "batch_params" | ||
case fptiEvents = "event_params" | ||
} | ||
} | ||
|
||
/// Encapsulates a single event by it's name and timestamp. | ||
struct Event: Codable { | ||
|
||
let eventName: String | ||
|
||
let timestamp: String = String(Date().utcTimestampMilliseconds) | ||
|
||
let tenantName: String = "Braintree" | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case eventName = "event_name" | ||
case timestamp = "t" | ||
case tenantName = "tenant_name" | ||
} | ||
} | ||
|
||
/// The FPTI tags/metadata applicable to all events in the batch upload. | ||
struct Metadata: Codable { | ||
|
||
let appID: String = Bundle.main.infoDictionary?[kCFBundleIdentifierKey as String] as? String ?? "N/A" | ||
|
||
let appName: String = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "N/A" | ||
|
||
let clientSDKVersion: String = Bundle.clientSDKVersion | ||
|
||
let clientOS: String = UIDevice.current.systemName + " " + UIDevice.current.systemVersion | ||
|
||
let component: String = "popupbridgesdk" | ||
|
||
let deviceManufacturer: String = "Apple" | ||
|
||
let deviceModel: String = { | ||
var systemInfo = utsname() | ||
uname(&systemInfo) | ||
let machineMirror = Mirror(reflecting: systemInfo.machine) | ||
let identifier = machineMirror.children.reduce("") { identifier, element in | ||
guard let value = element.value as? Int8, value != 0 else { return identifier } | ||
return identifier + String(UnicodeScalar(UInt8(value))) | ||
} | ||
return identifier | ||
}() | ||
|
||
let eventSource: String = "mobile-native" | ||
|
||
let isSimulator: Bool = { | ||
#if targetEnvironment(simulator) | ||
true | ||
#else | ||
false | ||
#endif | ||
}() | ||
|
||
let merchantAppVersion: String = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? "N/A" | ||
|
||
let packageManager: String = { | ||
#if COCOAPODS | ||
"CocoaPods" | ||
#elseif SWIFT_PACKAGE | ||
"Swift Package Manager" | ||
#else | ||
"Carthage or Other" | ||
#endif | ||
}() | ||
|
||
let platform: String = "iOS" | ||
|
||
let sessionID: String | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case appID = "app_id" | ||
case appName = "app_name" | ||
case clientSDKVersion = "c_sdk_ver" | ||
case clientOS = "client_os" | ||
case component = "comp" | ||
case deviceManufacturer = "device_manufacturer" | ||
case deviceModel = "mobile_device_model" | ||
case eventSource = "event_source" | ||
case isSimulator = "is_simulator" | ||
case merchantAppVersion = "mapv" | ||
case packageManager = "ios_package_manager" | ||
case platform | ||
case sessionID = "session_id" | ||
} | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Foundation | ||
|
||
enum NetworkError: Error { | ||
case invalidResponse | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thoughts on passing the statusCode so it can be logged?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this way it remains as basic as possible, which is what's expected, since im using this in the NetworkClient, the scope of httpResponse is limited within the guard block, so i cannot directly use it in the throw statement outside of that block. If in the future we need to add more endpoints, I think that would add more value. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add all of the new files that were specifically added for analytics to an
Analytics
folder underSources/PopupBridge
? nitpick, but would help navigation of the PopupBridge source filesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated: c8fd4cf