Skip to content

Commit

Permalink
NativeLogger + HyperOTA
Browse files Browse the repository at this point in the history
  • Loading branch information
manideepk90 committed Feb 11, 2025
1 parent 6fd8b25 commit f698b2f
Show file tree
Hide file tree
Showing 11 changed files with 636 additions and 16 deletions.
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ end

target 'hyperswitch' do
config = use_native_modules!

pod 'HyperOTA'
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => true,
Expand Down
86 changes: 82 additions & 4 deletions hyperswitch.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions hyperswitchSDK/Core/NativeLogger/Debouncer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Debouncer.swift
// hyperswitch
//
// Created by Kuntimaddi Manideep on 24/01/25.
//
import Foundation

final class Debouncer {
private let delay: TimeInterval
private var workItem: DispatchWorkItem?
private let queue: DispatchQueue
private let lock = NSLock()

init(delayInMillis: TimeInterval, queue: DispatchQueue = .main) {
self.delay = delayInMillis / 1000.0
self.queue = queue
}

func debounce(action: @escaping @Sendable () -> Void) {
lock.lock()
defer { lock.unlock() }
workItem?.cancel()
let task = DispatchWorkItem { action() }
workItem = task
queue.asyncAfter(deadline: .now() + delay, execute: task)
}

func cancel() {
lock.lock()
defer { lock.unlock() }

workItem?.cancel()
workItem = nil
}
}
116 changes: 116 additions & 0 deletions hyperswitchSDK/Core/NativeLogger/HyperLogManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// HyperLogManager.swift
// hyperswitch
//
// Created by Kuntimaddi Manideep on 24/01/25.
//

import Foundation

final class HyperLogManager {
private static var logsBatch: [Log] = []
private static var publishableKey: String = ""
private static var loggingEndPoint: String?
private static let fileManager = LogFileManager()
private static let queue = DispatchQueue(label: "hyperlog.queue")
private static let debouncer = Debouncer(delayInMillis: 2000.0)

private static func formatPayload(logs: [String]) -> String {
return "[" + logs.joined(separator: ",")
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "\n", with: "") + "]"
}

private static func getStringifiedLogs(_ logBatch: [Log]) -> [String] {
return logBatch.compactMap { $0.toJson() }
}

static func initialize(publishableKey: String) {
queue.async {
self.publishableKey = publishableKey
self.loggingEndPoint = SDKEnvironment.loggingURL(for: publishableKey)
}
if !publishableKey.isEmpty {
sendLogsFromFile()
}
}

static func sendLogsFromFile() {
queue.async {
guard let endpoint = loggingEndPoint else { return }
let logs = fileManager.getStoredLogsInArray()
let payload = formatPayload(logs: logs)
Task {
do {
let response = try await HyperNetworking.makeHttpRequest(
urlString: endpoint,
method: "POST",
headers: ["Content-Type": "application/json"],
body: payload)
if !response.isEmpty {
fileManager.clearFile()
}
} catch {
print("Network error")
}
}
}
}

static func saveLogsToFile() {
queue.async {
let logs = getStringifiedLogs(logsBatch)
fileManager.addLogs(logs: logs)
}
}

static func saveLogsToFile(_ logsBatch: [Log]) {
queue.async {
let logs = getStringifiedLogs(logsBatch)
fileManager.addLogs(logs: logs)
}
}

static func addLog(_ log: Log) {
queue.async {
logsBatch.append(log)
debouncer.debounce {
sendLogsOverNetwork()
}
}
}

private static func sendLogsOverNetwork() {
queue.async {
guard !logsBatch.isEmpty, let endpoint = loggingEndPoint else { return }
var copiedLogs = logsBatch
logsBatch.removeAll()

for index in copiedLogs.indices {
copiedLogs[index].merchant_id = HyperLogManager.publishableKey
}

let logsToSend = getStringifiedLogs(copiedLogs)

guard !logsToSend.isEmpty else { return }

let payload = formatPayload(logs: logsToSend)

Task {
do {
let response = try await HyperNetworking.makeHttpRequest(
urlString: endpoint,
method: "POST",
headers: ["Content-Type": "application/json"],
body: payload)

if response.isEmpty {
saveLogsToFile(copiedLogs)
}
} catch {
saveLogsToFile(copiedLogs)
}
}
}
}
}
85 changes: 85 additions & 0 deletions hyperswitchSDK/Core/NativeLogger/LogFileManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// LogFileManager.swift
// hyperswitch
//
// Created by Kuntimaddi Manideep on 24/01/25.
//
import Foundation

class LogFileManager {
private let logFileName = "crash_logs.json"
private let fileManager = FileManager.default
private let queue = DispatchQueue(label: "logfile.queue", attributes: .concurrent)

private func getLogFileName() -> URL? {
guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Failed to get document directory")
return nil
}
return documentDirectory.appendingPathComponent(logFileName)
}

func addLogs(logs: [String]) {
queue.async(flags: .barrier) { [weak self] in
guard let self = self, let fileURL = self.getLogFileName() else { return }

do {
var existingLogs = self.getStoredLogs() ?? "[]"
var logsArray = self.parseLogs(existingLogs)

logsArray.append(contentsOf: logs)

let jsonData = try JSONSerialization.data(withJSONObject: logsArray, options: .prettyPrinted)

try jsonData.write(to: fileURL, options: .atomic)
} catch {
print("Failed to write logs: \(error.localizedDescription)")
}
}
}

func getStoredLogs() -> String? {
guard let fileURL = getLogFileName(), fileManager.fileExists(atPath: fileURL.path) else {
return nil
}

do {
let data = try Data(contentsOf: fileURL)
if let jsonString = String(data: data, encoding: .utf8) {
return jsonString
} else {
return nil
}

} catch {
print("Error reading logs: \(error.localizedDescription)")
return nil
}
}


private func parseLogs(_ logs: String) -> [String] {
if let data = logs.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String] {
return json
}
return []
}

func getStoredLogsInArray() -> [String] {
return self.parseLogs(self.getStoredLogs() ?? "")
}

func clearFile() {
queue.async(flags: .barrier) { [weak self] in
guard let self = self, let fileURL = self.getLogFileName() else { return }
do {
if self.fileManager.fileExists(atPath: fileURL.path) {
try self.fileManager.removeItem(at: fileURL)
}
} catch {
print("Error clearing crash log file: \(error.localizedDescription)")
}
}
}
}
142 changes: 142 additions & 0 deletions hyperswitchSDK/Core/NativeLogger/LogType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//
// LogBuilder.swift
// hyperswitch
//
// Created by Kuntimaddi Manideep on 24/01/25.
//

import Foundation

enum LogType: String, Codable {
case DEBUG, INFO, ERROR, WARNING
}

enum LogCategory: String, Codable {
case API, USER_ERROR, USER_EVENT, MERCHANT_EVENT, OTA_LIFE_CYCLE
}

enum EventName: String, Codable {
case HYPER_OTA_INIT, HYPER_OTA_FINISH , HYPER_OTA_EVENT
}

struct Log: Codable {
let timestamp: String
let log_type: LogType
let component: String
let category: LogCategory
let version: String
let code_push_version: String
let client_core_version: String
let value: String
let internal_metadata: String
let session_id: String
var merchant_id: String
let payment_id: String
let app_id: String?
let platform: String
let user_agent: String
let event_name: EventName
let latency: String?
let first_event: String
let payment_method: String?
let payment_experience: String?
let source: String

func toJson() -> String? {
do {
let jsonData = try JSONEncoder().encode(self)
return String(data: jsonData, encoding: .utf8)
} catch {
print("Error encoding log to JSON: \(error.localizedDescription)")
return nil
}
}
}




class LogBuilder {
private var timestamp: String = ""
private var logType: LogType = .INFO
private var component: String = "MOBILE"
private var category: LogCategory = .OTA_LIFE_CYCLE
private var version: String = SDKVersion.current
private var codePushVersion: String = "0.0.2"
private var clientCoreVersion: String = ""
private var value: String = ""
private var internalMetadata: String = ""
private var sessionId: String = ""
private var merchantId: String = ""
private var paymentId: String = ""
private var appId: String? = nil
private var platform: String = "IOS"
private var userAgent: String = ""
private var eventName: EventName = .HYPER_OTA_INIT
private var latency: String? = nil
private var firstEvent: Bool = false
private var paymentMethod: String? = nil
private var paymentExperience: String? = nil
private var source: String = ""

func setLogType(_ logType: String) -> LogBuilder {
switch logType.uppercased() {
case "ERROR":
self.logType = .ERROR
break
case "WARNING":
self.logType = .WARNING
break
case "DEBUG":
self.logType = .DEBUG
break
default:
self.logType = .INFO
}
return self
}

func setClientCoreVersion(_ clientCoreVersion: String) -> LogBuilder {
self.clientCoreVersion = clientCoreVersion
return self
}

func setValue(_ value: String) -> LogBuilder {
self.value = value
return self
}

func setEventName(_ eventName: EventName) -> LogBuilder {
self.eventName = eventName
return self
}

func build() -> Log {
self.timestamp = String(Int(Date().timeIntervalSince1970 * 1000))
return Log(
timestamp: timestamp,
log_type: logType,
component: component,
category: category,
version: version,
code_push_version: codePushVersion,
client_core_version: clientCoreVersion,
value: value,
internal_metadata: internalMetadata,
session_id: sessionId,
merchant_id: merchantId,
payment_id: paymentId,
app_id: appId,
platform: platform,
user_agent: userAgent,
event_name: eventName,
latency: latency,
first_event: firstEvent ? "true" : "false",
payment_method: paymentMethod,
payment_experience: paymentExperience,
source: source
)
}
}


Loading

0 comments on commit f698b2f

Please sign in to comment.