@rejourneyco/react-native 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +29 -0
  2. package/android/build.gradle.kts +135 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/proguard-rules.pro +1 -0
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
  7. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
  8. package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
  9. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
  10. package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
  11. package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
  12. package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
  13. package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
  14. package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
  15. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
  16. package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
  17. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
  18. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
  19. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
  20. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
  21. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
  22. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
  23. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
  24. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
  25. package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
  26. package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
  27. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
  28. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  29. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
  30. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  31. package/ios/Engine/DeviceRegistrar.swift +288 -0
  32. package/ios/Engine/DiagnosticLog.swift +387 -0
  33. package/ios/Engine/RejourneyImpl.swift +719 -0
  34. package/ios/Recording/AnrSentinel.swift +142 -0
  35. package/ios/Recording/EventBuffer.swift +326 -0
  36. package/ios/Recording/InteractionRecorder.swift +428 -0
  37. package/ios/Recording/ReplayOrchestrator.swift +624 -0
  38. package/ios/Recording/SegmentDispatcher.swift +492 -0
  39. package/ios/Recording/StabilityMonitor.swift +223 -0
  40. package/ios/Recording/TelemetryPipeline.swift +547 -0
  41. package/ios/Recording/ViewHierarchyScanner.swift +156 -0
  42. package/ios/Recording/VisualCapture.swift +675 -0
  43. package/ios/Rejourney.h +38 -0
  44. package/ios/Rejourney.mm +375 -0
  45. package/ios/Utility/DataCompression.swift +55 -0
  46. package/ios/Utility/ImageBlur.swift +89 -0
  47. package/ios/Utility/RuntimeMethodSwap.swift +41 -0
  48. package/ios/Utility/ViewIdentifier.swift +37 -0
  49. package/lib/commonjs/NativeRejourney.js +40 -0
  50. package/lib/commonjs/components/Mask.js +88 -0
  51. package/lib/commonjs/index.js +1443 -0
  52. package/lib/commonjs/sdk/autoTracking.js +1087 -0
  53. package/lib/commonjs/sdk/constants.js +166 -0
  54. package/lib/commonjs/sdk/errorTracking.js +187 -0
  55. package/lib/commonjs/sdk/index.js +50 -0
  56. package/lib/commonjs/sdk/metricsTracking.js +205 -0
  57. package/lib/commonjs/sdk/navigation.js +128 -0
  58. package/lib/commonjs/sdk/networkInterceptor.js +375 -0
  59. package/lib/commonjs/sdk/utils.js +433 -0
  60. package/lib/commonjs/sdk/version.js +13 -0
  61. package/lib/commonjs/types/expo-router.d.js +2 -0
  62. package/lib/commonjs/types/index.js +2 -0
  63. package/lib/module/NativeRejourney.js +38 -0
  64. package/lib/module/components/Mask.js +83 -0
  65. package/lib/module/index.js +1341 -0
  66. package/lib/module/sdk/autoTracking.js +1059 -0
  67. package/lib/module/sdk/constants.js +154 -0
  68. package/lib/module/sdk/errorTracking.js +177 -0
  69. package/lib/module/sdk/index.js +26 -0
  70. package/lib/module/sdk/metricsTracking.js +187 -0
  71. package/lib/module/sdk/navigation.js +120 -0
  72. package/lib/module/sdk/networkInterceptor.js +364 -0
  73. package/lib/module/sdk/utils.js +412 -0
  74. package/lib/module/sdk/version.js +7 -0
  75. package/lib/module/types/expo-router.d.js +2 -0
  76. package/lib/module/types/index.js +2 -0
  77. package/lib/typescript/NativeRejourney.d.ts +160 -0
  78. package/lib/typescript/components/Mask.d.ts +54 -0
  79. package/lib/typescript/index.d.ts +117 -0
  80. package/lib/typescript/sdk/autoTracking.d.ts +226 -0
  81. package/lib/typescript/sdk/constants.d.ts +138 -0
  82. package/lib/typescript/sdk/errorTracking.d.ts +47 -0
  83. package/lib/typescript/sdk/index.d.ts +24 -0
  84. package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
  85. package/lib/typescript/sdk/navigation.d.ts +48 -0
  86. package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
  87. package/lib/typescript/sdk/utils.d.ts +193 -0
  88. package/lib/typescript/sdk/version.d.ts +6 -0
  89. package/lib/typescript/types/index.d.ts +618 -0
  90. package/package.json +122 -0
  91. package/rejourney.podspec +23 -0
  92. package/src/NativeRejourney.ts +185 -0
  93. package/src/components/Mask.tsx +93 -0
  94. package/src/index.ts +1555 -0
  95. package/src/sdk/autoTracking.ts +1245 -0
  96. package/src/sdk/constants.ts +155 -0
  97. package/src/sdk/errorTracking.ts +231 -0
  98. package/src/sdk/index.ts +25 -0
  99. package/src/sdk/metricsTracking.ts +227 -0
  100. package/src/sdk/navigation.ts +152 -0
  101. package/src/sdk/networkInterceptor.ts +423 -0
  102. package/src/sdk/utils.ts +442 -0
  103. package/src/sdk/version.ts +6 -0
  104. package/src/types/expo-router.d.ts +7 -0
  105. package/src/types/index.ts +709 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import UIKit
18
+ import CommonCrypto
19
+
20
+ // MARK: - Device Registrar
21
+
22
+ /// Establishes device identity and obtains upload credentials
23
+ @objc(DeviceRegistrar)
24
+ public final class DeviceRegistrar: NSObject {
25
+
26
+ @objc public static let shared = DeviceRegistrar()
27
+
28
+ // MARK: Public Configuration
29
+
30
+ @objc public var endpoint = "https://api.rejourney.co"
31
+ @objc public var apiToken: String?
32
+
33
+ @objc public private(set) var deviceFingerprint: String?
34
+ @objc public private(set) var uploadCredential: String?
35
+ @objc public private(set) var credentialValid = false
36
+
37
+ // MARK: Private State
38
+
39
+ private let _keychainId = "com.rejourney.device.fingerprint"
40
+
41
+ private lazy var _httpSession: URLSession = {
42
+ let config = URLSessionConfiguration.default
43
+ config.timeoutIntervalForRequest = 30
44
+ config.timeoutIntervalForResource = 60
45
+ return URLSession(configuration: config)
46
+ }()
47
+
48
+ // MARK: Lifecycle
49
+
50
+ private override init() {
51
+ super.init()
52
+ _establishIdentity()
53
+ }
54
+
55
+ // MARK: Credential Management
56
+
57
+ @objc public func obtainCredential(apiToken: String, completion: @escaping (Bool, String?) -> Void) {
58
+ self.apiToken = apiToken
59
+
60
+ guard let fingerprint = deviceFingerprint else {
61
+ completion(false, "Device identity unavailable")
62
+ return
63
+ }
64
+
65
+ _fetchServerCredential(fingerprint: fingerprint, apiToken: apiToken, completion: completion)
66
+ }
67
+
68
+ @objc public func invalidateCredential() {
69
+ uploadCredential = nil
70
+ credentialValid = false
71
+ }
72
+
73
+ // MARK: Device Profile
74
+
75
+ @objc public func gatherDeviceProfile() -> [String: Any] {
76
+ let device = UIDevice.current
77
+ let screen = UIScreen.main
78
+
79
+ return [
80
+ "fingerprint": deviceFingerprint ?? "",
81
+ "os": "ios",
82
+ "hwModel": _resolveHardwareModel(),
83
+ "osRelease": device.systemVersion,
84
+ "appRelease": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown",
85
+ "buildId": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown",
86
+ "displayWidth": Int(screen.bounds.width * screen.scale),
87
+ "displayHeight": Int(screen.bounds.height * screen.scale),
88
+ "displayDensity": screen.scale,
89
+ "region": Locale.current.identifier,
90
+ "tz": TimeZone.current.identifier,
91
+ "simulated": _isSimulator()
92
+ ]
93
+ }
94
+
95
+ public func composeAuthHeaders() -> [String: String] {
96
+ var headers: [String: String] = [:]
97
+
98
+ if let token = apiToken {
99
+ headers["x-rejourney-key"] = token
100
+ }
101
+ if let credential = uploadCredential {
102
+ headers["x-upload-token"] = credential
103
+ }
104
+
105
+ return headers
106
+ }
107
+
108
+ // MARK: Identity Establishment
109
+
110
+ private func _establishIdentity() {
111
+ if let stored = _keychainLoad(_keychainId) {
112
+ deviceFingerprint = stored
113
+ return
114
+ }
115
+
116
+ let fresh = _generateFingerprint()
117
+ deviceFingerprint = fresh
118
+ _keychainSave(_keychainId, value: fresh)
119
+ }
120
+
121
+ private func _generateFingerprint() -> String {
122
+ let device = UIDevice.current
123
+ let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
124
+
125
+ var composite = bundleId
126
+ composite += device.model
127
+ composite += device.systemName
128
+ composite += device.systemVersion
129
+ composite += device.identifierForVendor?.uuidString ?? UUID().uuidString
130
+
131
+ return _sha256(composite)
132
+ }
133
+
134
+ // MARK: Server Communication
135
+
136
+ private func _fetchServerCredential(fingerprint: String, apiToken: String, completion: @escaping (Bool, String?) -> Void) {
137
+ let requestStartTime = CFAbsoluteTimeGetCurrent()
138
+ DiagnosticLog.debugCredentialFlow(phase: "START", fingerprint: fingerprint, success: true, detail: "apiToken=\(apiToken.prefix(12))...")
139
+
140
+ guard let url = URL(string: "\(endpoint)/api/ingest/auth/device") else {
141
+ DiagnosticLog.debugCredentialFlow(phase: "ERROR", fingerprint: fingerprint, success: false, detail: "Malformed endpoint URL")
142
+ completion(false, "Malformed endpoint URL")
143
+ return
144
+ }
145
+
146
+ var req = URLRequest(url: url)
147
+ req.httpMethod = "POST"
148
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
149
+ req.setValue(apiToken, forHTTPHeaderField: "x-rejourney-key")
150
+
151
+ let profile = gatherDeviceProfile()
152
+ let payload: [String: Any] = [
153
+ "deviceId": fingerprint,
154
+ "metadata": profile
155
+ ]
156
+
157
+ DiagnosticLog.debugNetworkRequest(method: "POST", url: url.absoluteString, headers: ["x-rejourney-key": apiToken])
158
+
159
+ do {
160
+ req.httpBody = try JSONSerialization.data(withJSONObject: payload)
161
+ DiagnosticLog.debugCredentialFlow(phase: "PAYLOAD", fingerprint: fingerprint, success: true, detail: "size=\(req.httpBody?.count ?? 0)B")
162
+ } catch {
163
+ DiagnosticLog.debugCredentialFlow(phase: "ERROR", fingerprint: fingerprint, success: false, detail: "Payload encoding failed: \(error)")
164
+ completion(false, "Payload encoding failed")
165
+ return
166
+ }
167
+
168
+ _httpSession.dataTask(with: req) { [weak self] data, response, error in
169
+ let requestDurationMs = (CFAbsoluteTimeGetCurrent() - requestStartTime) * 1000
170
+
171
+ guard let self else {
172
+ DiagnosticLog.debugCredentialFlow(phase: "ERROR", fingerprint: fingerprint, success: false, detail: "Instance released")
173
+ completion(false, "Instance released")
174
+ return
175
+ }
176
+
177
+ guard let data = data, let httpResp = response as? HTTPURLResponse else {
178
+ DiagnosticLog.debugCredentialFlow(phase: "FALLBACK", fingerprint: fingerprint, success: true, detail: "No response, using local credential error=\(error?.localizedDescription ?? "none")")
179
+ DiagnosticLog.debugNetworkResponse(url: url.absoluteString, status: 0, bodySize: 0, durationMs: requestDurationMs)
180
+
181
+ self.uploadCredential = self._synthesizeLocalCredential(fingerprint: fingerprint, apiToken: apiToken)
182
+ self.credentialValid = true
183
+ DispatchQueue.main.async { completion(true, self.uploadCredential) }
184
+ return
185
+ }
186
+
187
+ DiagnosticLog.debugNetworkResponse(url: url.absoluteString, status: httpResp.statusCode, bodySize: data.count, durationMs: requestDurationMs)
188
+
189
+ if httpResp.statusCode == 200 {
190
+ do {
191
+ if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
192
+ let token = json["uploadToken"] as? String {
193
+ DiagnosticLog.debugCredentialFlow(phase: "SUCCESS", fingerprint: fingerprint, success: true, detail: "Got server credential uploadToken=\(token.prefix(12))...")
194
+ self.uploadCredential = token
195
+ self.credentialValid = true
196
+ DispatchQueue.main.async { completion(true, token) }
197
+ return
198
+ }
199
+ } catch {
200
+ DiagnosticLog.debugCredentialFlow(phase: "PARSE_ERROR", fingerprint: fingerprint, success: false, detail: "\(error)")
201
+ }
202
+ } else {
203
+ let bodyPreview = String(data: data.prefix(200), encoding: .utf8) ?? "binary"
204
+ DiagnosticLog.debugCredentialFlow(phase: "HTTP_ERROR", fingerprint: fingerprint, success: false, detail: "status=\(httpResp.statusCode) body=\(bodyPreview)")
205
+ }
206
+
207
+ DiagnosticLog.debugCredentialFlow(phase: "FALLBACK", fingerprint: fingerprint, success: true, detail: "Using local credential after server error")
208
+ self.uploadCredential = self._synthesizeLocalCredential(fingerprint: fingerprint, apiToken: apiToken)
209
+ self.credentialValid = true
210
+ DispatchQueue.main.async { completion(true, self.uploadCredential) }
211
+ }.resume()
212
+ }
213
+
214
+ private func _synthesizeLocalCredential(fingerprint: String, apiToken: String) -> String {
215
+ let timestamp = Int(Date().timeIntervalSince1970)
216
+ let composite = "\(apiToken):\(fingerprint):\(timestamp)"
217
+ return _sha256(composite)
218
+ }
219
+
220
+ // MARK: Hardware Detection
221
+
222
+ private func _resolveHardwareModel() -> String {
223
+ var size: Int = 0
224
+ sysctlbyname("hw.machine", nil, &size, nil, 0)
225
+
226
+ var machine = [CChar](repeating: 0, count: size)
227
+ sysctlbyname("hw.machine", &machine, &size, nil, 0)
228
+
229
+ return String(cString: machine)
230
+ }
231
+
232
+ private func _isSimulator() -> Bool {
233
+ #if targetEnvironment(simulator)
234
+ return true
235
+ #else
236
+ return false
237
+ #endif
238
+ }
239
+
240
+ // MARK: Cryptographic Helpers
241
+
242
+ private func _sha256(_ input: String) -> String {
243
+ guard let data = input.data(using: .utf8) else { return "" }
244
+
245
+ var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
246
+ data.withUnsafeBytes { bytes in
247
+ _ = CC_SHA256(bytes.baseAddress, CC_LONG(data.count), &digest)
248
+ }
249
+
250
+ return digest.map { String(format: "%02x", $0) }.joined()
251
+ }
252
+
253
+ // MARK: Keychain Operations
254
+
255
+ private func _keychainSave(_ key: String, value: String) {
256
+ guard let data = value.data(using: .utf8) else { return }
257
+
258
+ let query: [String: Any] = [
259
+ kSecClass as String: kSecClassGenericPassword,
260
+ kSecAttrAccount as String: key,
261
+ kSecValueData as String: data,
262
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
263
+ ]
264
+
265
+ SecItemDelete(query as CFDictionary)
266
+ SecItemAdd(query as CFDictionary, nil)
267
+ }
268
+
269
+ private func _keychainLoad(_ key: String) -> String? {
270
+ let query: [String: Any] = [
271
+ kSecClass as String: kSecClassGenericPassword,
272
+ kSecAttrAccount as String: key,
273
+ kSecReturnData as String: true,
274
+ kSecMatchLimit as String: kSecMatchLimitOne
275
+ ]
276
+
277
+ var result: AnyObject?
278
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
279
+
280
+ guard status == errSecSuccess,
281
+ let data = result as? Data,
282
+ let value = String(data: data, encoding: .utf8) else {
283
+ return nil
284
+ }
285
+
286
+ return value
287
+ }
288
+ }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import Foundation
18
+ import QuartzCore
19
+
20
+ // MARK: - Log Level
21
+
22
+ /// Severity tiers for SDK diagnostic messages
23
+ @objc public enum LogLevel: Int {
24
+ case trace = 0
25
+ case notice = 1
26
+ case caution = 2
27
+ case fault = 3
28
+ }
29
+
30
+ // MARK: - Performance Snapshot
31
+
32
+ /// Captures point-in-time performance metrics
33
+ public struct PerformanceSnapshot {
34
+ let wallTimeMs: Double
35
+ let cpuTimeMs: Double
36
+ let mainThreadTimeMs: Double
37
+ let timestamp: CFAbsoluteTime
38
+ let isMainThread: Bool
39
+ let threadName: String
40
+
41
+ public static func capture() -> PerformanceSnapshot {
42
+ var taskInfo = mach_task_basic_info()
43
+ var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
44
+
45
+ var cpuTimeMs: Double = 0
46
+ if task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), withUnsafeMutablePointer(to: &taskInfo) { $0.withMemoryRebound(to: Int32.self, capacity: 1) { $0 } }, &count) == KERN_SUCCESS {
47
+ let userTimeMs = Double(taskInfo.user_time.seconds) * 1000.0 + Double(taskInfo.user_time.microseconds) / 1000.0
48
+ let systemTimeMs = Double(taskInfo.system_time.seconds) * 1000.0 + Double(taskInfo.system_time.microseconds) / 1000.0
49
+ cpuTimeMs = userTimeMs + systemTimeMs
50
+ }
51
+
52
+ let isMain = Thread.isMainThread
53
+ let name: String
54
+ if isMain {
55
+ name = "main"
56
+ } else if let threadName = Thread.current.name, !threadName.isEmpty {
57
+ name = threadName
58
+ } else {
59
+ name = "bg-\(String(format: "%04x", UInt16(truncatingIfNeeded: UInt(bitPattern: ObjectIdentifier(Thread.current)))))"
60
+ }
61
+
62
+ return PerformanceSnapshot(
63
+ wallTimeMs: CACurrentMediaTime() * 1000,
64
+ cpuTimeMs: cpuTimeMs,
65
+ mainThreadTimeMs: isMain ? CACurrentMediaTime() * 1000 : 0,
66
+ timestamp: CFAbsoluteTimeGetCurrent(),
67
+ isMainThread: isMain,
68
+ threadName: name
69
+ )
70
+ }
71
+
72
+ public func elapsed(since start: PerformanceSnapshot) -> (wall: Double, cpu: Double, thread: String) {
73
+ return (
74
+ wall: wallTimeMs - start.wallTimeMs,
75
+ cpu: cpuTimeMs - start.cpuTimeMs,
76
+ thread: isMainThread ? "🟢 MAIN" : "🔵 BG(\(threadName))"
77
+ )
78
+ }
79
+ }
80
+
81
+ // MARK: - Diagnostic Log
82
+
83
+ /// Centralized logging facility for SDK diagnostics
84
+ @objc(DiagnosticLog)
85
+ public final class DiagnosticLog: NSObject {
86
+
87
+ // MARK: Configuration
88
+
89
+ @objc public static var minimumLevel: Int = 1
90
+ @objc public static var includeTimestamp: Bool = true
91
+ @objc public static var detailedOutput: Bool = false
92
+ @objc public static var performanceTracing: Bool = false
93
+
94
+ // MARK: Level-Based Emission
95
+
96
+ public static func emit(_ level: LogLevel, _ message: String) {
97
+ guard level.rawValue >= minimumLevel else { return }
98
+
99
+ let prefix: String
100
+ switch level {
101
+ case .trace: prefix = "TRACE"
102
+ case .notice: prefix = "INFO"
103
+ case .caution: prefix = "WARN"
104
+ case .fault: prefix = "ERROR"
105
+ }
106
+
107
+ _writeLog(prefix: prefix, message: message)
108
+ }
109
+
110
+ // MARK: Convenience Methods
111
+
112
+ @objc public static func trace(_ message: String) {
113
+ guard minimumLevel <= 0 else { return }
114
+ _writeLog(prefix: "VERBOSE", message: message)
115
+ }
116
+
117
+ @objc public static func notice(_ message: String) {
118
+ guard minimumLevel <= 1 else { return }
119
+ _writeLog(prefix: "INFO", message: message)
120
+ }
121
+
122
+ @objc public static func caution(_ message: String) {
123
+ guard minimumLevel <= 2 else { return }
124
+ _writeLog(prefix: "WARN", message: message)
125
+ }
126
+
127
+ @objc public static func fault(_ message: String) {
128
+ guard minimumLevel <= 3 else { return }
129
+ _writeLog(prefix: "ERROR", message: message)
130
+ }
131
+
132
+ // MARK: Lifecycle Events
133
+
134
+ @objc public static func sdkReady(_ version: String) {
135
+ notice("[Rejourney] SDK initialized v\(version)")
136
+ }
137
+
138
+ @objc public static func sdkFailed(_ reason: String) {
139
+ fault("[Rejourney] Initialization failed: \(reason)")
140
+ }
141
+
142
+ @objc public static func replayBegan(_ sessionId: String) {
143
+ notice("[Rejourney] Recording started: \(sessionId)")
144
+ }
145
+
146
+ @objc public static func replayEnded(_ sessionId: String) {
147
+ notice("[Rejourney] Recording ended: \(sessionId)")
148
+ }
149
+
150
+ // MARK: Debug-Only Session Logs
151
+
152
+ public static func debugSessionCreate(phase: String, details: String, perf: PerformanceSnapshot? = nil) {
153
+ guard detailedOutput else { return }
154
+ var msg = "📍 [SESSION] \(phase): \(details)"
155
+ if let p = perf, performanceTracing {
156
+ let threadIcon = p.isMainThread ? "🟢 MAIN" : "🔵 BG"
157
+ msg += " | \(threadIcon) wall=\(String(format: "%.2f", p.wallTimeMs))ms cpu=\(String(format: "%.2f", p.cpuTimeMs))ms"
158
+ }
159
+ _writeLog(prefix: "DEBUG", message: msg)
160
+ }
161
+
162
+ public static func debugSessionTiming(operation: String, startPerf: PerformanceSnapshot, endPerf: PerformanceSnapshot) {
163
+ guard detailedOutput && performanceTracing else { return }
164
+ let elapsed = endPerf.elapsed(since: startPerf)
165
+ _writeLog(prefix: "PERF", message: "⏱️ [\(operation)] \(elapsed.thread) | wall=\(String(format: "%.2f", elapsed.wall))ms cpu=\(String(format: "%.2f", elapsed.cpu))ms")
166
+ }
167
+
168
+ // MARK: Enhanced Performance Logging
169
+
170
+ /// Log a timed operation with automatic thread detection
171
+ public static func perfOperation(_ name: String, category: String = "OP", block: () -> Void) {
172
+ guard detailedOutput && performanceTracing else {
173
+ block()
174
+ return
175
+ }
176
+
177
+ let start = PerformanceSnapshot.capture()
178
+ block()
179
+ let end = PerformanceSnapshot.capture()
180
+ let elapsed = end.elapsed(since: start)
181
+
182
+ let warningThreshold: Double = 16.67 // One frame at 60fps
183
+ let icon = elapsed.wall > warningThreshold ? "🔴" : (elapsed.wall > 8 ? "🟡" : "🟢")
184
+
185
+ _writeLog(prefix: "PERF", message: "\(icon) [\(category)] \(name) | \(elapsed.thread) | ⏱️ \(String(format: "%.2f", elapsed.wall))ms wall, \(String(format: "%.2f", elapsed.cpu))ms cpu")
186
+ }
187
+
188
+ /// Log a timed async operation start
189
+ public static func perfStart(_ name: String, category: String = "ASYNC") -> CFAbsoluteTime {
190
+ let start = CFAbsoluteTimeGetCurrent()
191
+ if detailedOutput && performanceTracing {
192
+ let threadInfo = Thread.isMainThread ? "🟢 MAIN" : "🔵 BG"
193
+ _writeLog(prefix: "PERF", message: "▶️ [\(category)] \(name) started | \(threadInfo)")
194
+ }
195
+ return start
196
+ }
197
+
198
+ /// Log a timed async operation end
199
+ public static func perfEnd(_ name: String, startTime: CFAbsoluteTime, category: String = "ASYNC", success: Bool = true) {
200
+ guard detailedOutput && performanceTracing else { return }
201
+ let elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
202
+ let threadInfo = Thread.isMainThread ? "🟢 MAIN" : "🔵 BG"
203
+ let icon = success ? "✅" : "❌"
204
+
205
+ let warningThreshold: Double = 100 // 100ms for async ops
206
+ let timeIcon = elapsed > warningThreshold ? "🔴" : (elapsed > 50 ? "🟡" : "🟢")
207
+
208
+ _writeLog(prefix: "PERF", message: "\(icon) [\(category)] \(name) finished | \(threadInfo) | \(timeIcon) \(String(format: "%.2f", elapsed))ms")
209
+ }
210
+
211
+ /// Log frame timing for visual capture
212
+ public static func perfFrame(operation: String, durationMs: Double, frameNumber: Int, isMainThread: Bool) {
213
+ guard detailedOutput && performanceTracing else { return }
214
+ let threadInfo = isMainThread ? "🟢 MAIN" : "🔵 BG"
215
+ let budget: Double = 33.33 // 30fps budget
216
+ let icon = durationMs > budget ? "🔴 DROPPED" : (durationMs > 16.67 ? "🟡 SLOW" : "🟢 OK")
217
+
218
+ _writeLog(prefix: "FRAME", message: "🎬 [\(operation)] #\(frameNumber) | \(threadInfo) | \(icon) \(String(format: "%.2f", durationMs))ms (budget: \(String(format: "%.1f", budget))ms)")
219
+ }
220
+
221
+ /// Log batch operation timing
222
+ public static func perfBatch(operation: String, itemCount: Int, totalMs: Double, isMainThread: Bool) {
223
+ guard detailedOutput && performanceTracing else { return }
224
+ let threadInfo = isMainThread ? "🟢 MAIN" : "🔵 BG"
225
+ let avgMs = itemCount > 0 ? totalMs / Double(itemCount) : 0
226
+
227
+ _writeLog(prefix: "BATCH", message: "📦 [\(operation)] \(itemCount) items | \(threadInfo) | total=\(String(format: "%.2f", totalMs))ms avg=\(String(format: "%.3f", avgMs))ms/item")
228
+ }
229
+
230
+ /// Log network timing with throughput
231
+ public static func perfNetwork(operation: String, url: String, durationMs: Double, bytesTransferred: Int, success: Bool) {
232
+ guard detailedOutput && performanceTracing else { return }
233
+ let threadInfo = Thread.isMainThread ? "🟢 MAIN" : "🔵 BG"
234
+ let throughputKBps = durationMs > 0 ? Double(bytesTransferred) / durationMs : 0
235
+ let icon = success ? "✅" : "❌"
236
+ let shortUrl = url.components(separatedBy: "/").suffix(2).joined(separator: "/")
237
+
238
+ _writeLog(prefix: "NET", message: "\(icon) [\(operation)] \(shortUrl) | \(threadInfo) | \(String(format: "%.2f", durationMs))ms, \(bytesTransferred)B @ \(String(format: "%.1f", throughputKBps))KB/s")
239
+ }
240
+
241
+ // MARK: Debug-Only Presign Logs
242
+
243
+ public static func debugPresignRequest(url: String, sessionId: String, kind: String, sizeBytes: Int) {
244
+ guard detailedOutput else { return }
245
+ _writeLog(prefix: "DEBUG", message: "🔐 [PRESIGN-REQ] url=\(url) sessionId=\(sessionId) kind=\(kind) size=\(sizeBytes)B")
246
+ }
247
+
248
+ public static func debugPresignResponse(status: Int, segmentId: String?, uploadUrl: String?, durationMs: Double) {
249
+ guard detailedOutput else { return }
250
+ if let segId = segmentId, let url = uploadUrl {
251
+ let truncUrl = url.count > 80 ? String(url.prefix(80)) + "..." : url
252
+ _writeLog(prefix: "DEBUG", message: "✅ [PRESIGN-OK] status=\(status) segmentId=\(segId) uploadUrl=\(truncUrl) took=\(String(format: "%.1f", durationMs))ms")
253
+ } else {
254
+ _writeLog(prefix: "DEBUG", message: "❌ [PRESIGN-FAIL] status=\(status) took=\(String(format: "%.1f", durationMs))ms")
255
+ }
256
+ }
257
+
258
+ public static func debugUploadProgress(phase: String, segmentId: String, bytesWritten: Int64, totalBytes: Int64) {
259
+ guard detailedOutput else { return }
260
+ let pct = totalBytes > 0 ? Double(bytesWritten) / Double(totalBytes) * 100 : 0
261
+ _writeLog(prefix: "DEBUG", message: "📤 [UPLOAD] \(phase) segmentId=\(segmentId) progress=\(String(format: "%.1f", pct))% (\(bytesWritten)/\(totalBytes)B)")
262
+ }
263
+
264
+ public static func debugUploadComplete(segmentId: String, status: Int, durationMs: Double, throughputKBps: Double) {
265
+ guard detailedOutput else { return }
266
+ _writeLog(prefix: "DEBUG", message: "📤 [UPLOAD-DONE] segmentId=\(segmentId) status=\(status) took=\(String(format: "%.1f", durationMs))ms throughput=\(String(format: "%.1f", throughputKBps))KB/s")
267
+ }
268
+
269
+ // MARK: Debug-Only Network Logs
270
+
271
+ public static func debugNetworkRequest(method: String, url: String, headers: [String: String]?) {
272
+ guard detailedOutput else { return }
273
+ var msg = "🌐 [NET-REQ] \(method) \(url)"
274
+ if let h = headers {
275
+ let sanitized = h.mapValues { $0.count > 20 ? String($0.prefix(8)) + "..." : $0 }
276
+ msg += " headers=\(sanitized)"
277
+ }
278
+ _writeLog(prefix: "DEBUG", message: msg)
279
+ }
280
+
281
+ public static func debugNetworkResponse(url: String, status: Int, bodySize: Int, durationMs: Double) {
282
+ guard detailedOutput else { return }
283
+ _writeLog(prefix: "DEBUG", message: "🌐 [NET-RSP] \(url.components(separatedBy: "/").last ?? url) status=\(status) size=\(bodySize)B took=\(String(format: "%.1f", durationMs))ms")
284
+ }
285
+
286
+ // MARK: Debug-Only Credential Logs
287
+
288
+ public static func debugCredentialFlow(phase: String, fingerprint: String?, success: Bool, detail: String = "") {
289
+ guard detailedOutput else { return }
290
+ let fp = fingerprint.map { String($0.prefix(12)) + "..." } ?? "nil"
291
+ let icon = success ? "✅" : "❌"
292
+ _writeLog(prefix: "DEBUG", message: "\(icon) [CRED] \(phase) fingerprint=\(fp) \(detail)")
293
+ }
294
+
295
+ // MARK: Debug-Only Storage Logs
296
+
297
+ public static func debugStorage(op: String, key: String, success: Bool, detail: String = "") {
298
+ guard detailedOutput else { return }
299
+ let icon = success ? "✅" : "❌"
300
+ _writeLog(prefix: "DEBUG", message: "\(icon) [STORAGE] \(op) key=\(key) \(detail)")
301
+ }
302
+
303
+ // MARK: Debug-Only Performance Logs
304
+
305
+ public static func debugPerformanceMarker(_ operation: String, startTime: CFAbsoluteTime, context: String = "") {
306
+ guard detailedOutput && performanceTracing else { return }
307
+ let elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
308
+ let threadInfo = Thread.isMainThread ? "🟢 MAIN" : "🔵 BG"
309
+ let warningIcon = elapsed > 16.67 ? "🔴" : (elapsed > 8 ? "🟡" : "🟢")
310
+ var msg = "\(warningIcon) [\(operation)] \(threadInfo) | \(String(format: "%.2f", elapsed))ms"
311
+ if !context.isEmpty { msg += " | \(context)" }
312
+ _writeLog(prefix: "PERF", message: msg)
313
+ }
314
+
315
+ public static func debugMemoryUsage(context: String) {
316
+ guard detailedOutput && performanceTracing else { return }
317
+ var info = mach_task_basic_info()
318
+ var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
319
+ let result = withUnsafeMutablePointer(to: &info) {
320
+ $0.withMemoryRebound(to: Int32.self, capacity: 1) { task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) }
321
+ }
322
+ if result == KERN_SUCCESS {
323
+ let usedMB = Double(info.resident_size) / 1_048_576
324
+ let virtualMB = Double(info.virtual_size) / 1_048_576
325
+ let warningIcon = usedMB > 100 ? "🔴" : (usedMB > 50 ? "🟡" : "🟢")
326
+ _writeLog(prefix: "MEM", message: "\(warningIcon) [\(context)] resident=\(String(format: "%.1f", usedMB))MB virtual=\(String(format: "%.1f", virtualMB))MB")
327
+ }
328
+ }
329
+
330
+ public static func debugCPUUsage(context: String) {
331
+ guard detailedOutput && performanceTracing else { return }
332
+ var threadsList: thread_act_array_t?
333
+ var threadCount: mach_msg_type_number_t = 0
334
+ guard task_threads(mach_task_self_, &threadsList, &threadCount) == KERN_SUCCESS, let threads = threadsList else { return }
335
+
336
+ var totalCPU: Double = 0
337
+ var mainThreadCPU: Double = 0
338
+ let threadInfoCount = mach_msg_type_number_t(MemoryLayout<thread_basic_info_data_t>.size / MemoryLayout<natural_t>.size)
339
+
340
+ for i in 0..<Int(threadCount) {
341
+ var info = thread_basic_info()
342
+ var infoCount = threadInfoCount
343
+ let result = withUnsafeMutablePointer(to: &info) {
344
+ $0.withMemoryRebound(to: Int32.self, capacity: 1) { thread_info(threads[i], thread_flavor_t(THREAD_BASIC_INFO), $0, &infoCount) }
345
+ }
346
+ if result == KERN_SUCCESS, info.flags & TH_FLAGS_IDLE == 0 {
347
+ let cpuUsage = Double(info.cpu_usage) / Double(TH_USAGE_SCALE) * 100
348
+ totalCPU += cpuUsage
349
+ if i == 0 { mainThreadCPU = cpuUsage } // First thread is typically main
350
+ }
351
+ }
352
+
353
+ vm_deallocate(mach_task_self_, vm_address_t(bitPattern: threads), vm_size_t(threadCount) * vm_size_t(MemoryLayout<thread_t>.size))
354
+
355
+ let warningIcon = totalCPU > 80 ? "🔴" : (totalCPU > 50 ? "🟡" : "🟢")
356
+ _writeLog(prefix: "CPU", message: "\(warningIcon) [\(context)] 🟢 main=\(String(format: "%.1f", mainThreadCPU))% | total=\(String(format: "%.1f", totalCPU))% across \(threadCount) threads")
357
+ }
358
+
359
+ // MARK: Configuration
360
+
361
+ @objc public static func setVerbose(_ enabled: Bool) {
362
+ detailedOutput = enabled
363
+ performanceTracing = enabled
364
+ minimumLevel = enabled ? 0 : 1
365
+ if enabled {
366
+ _writeLog(prefix: "INFO", message: "🔧 [CONFIG] Debug mode ENABLED: detailedOutput=\\(detailedOutput), performanceTracing=\\(performanceTracing), minimumLevel=\\(minimumLevel)")
367
+ }
368
+ }
369
+
370
+ // MARK: Private Implementation
371
+
372
+ private static func _writeLog(prefix: String, message: String) {
373
+ var output = "[RJ]"
374
+
375
+ if includeTimestamp {
376
+ let formatter = ISO8601DateFormatter()
377
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
378
+ output += " \(formatter.string(from: Date()))"
379
+ }
380
+
381
+ output += " [\(prefix)] \(message)"
382
+ print(output)
383
+ }
384
+ }
385
+
386
+ // Type alias for backward compatibility
387
+ typealias LogSeverity = LogLevel