@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.
- package/README.md +29 -0
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
- package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
- package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
- package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
- package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
- package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
- package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
- package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
- package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
- package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Engine/DeviceRegistrar.swift +288 -0
- package/ios/Engine/DiagnosticLog.swift +387 -0
- package/ios/Engine/RejourneyImpl.swift +719 -0
- package/ios/Recording/AnrSentinel.swift +142 -0
- package/ios/Recording/EventBuffer.swift +326 -0
- package/ios/Recording/InteractionRecorder.swift +428 -0
- package/ios/Recording/ReplayOrchestrator.swift +624 -0
- package/ios/Recording/SegmentDispatcher.swift +492 -0
- package/ios/Recording/StabilityMonitor.swift +223 -0
- package/ios/Recording/TelemetryPipeline.swift +547 -0
- package/ios/Recording/ViewHierarchyScanner.swift +156 -0
- package/ios/Recording/VisualCapture.swift +675 -0
- package/ios/Rejourney.h +38 -0
- package/ios/Rejourney.mm +375 -0
- package/ios/Utility/DataCompression.swift +55 -0
- package/ios/Utility/ImageBlur.swift +89 -0
- package/ios/Utility/RuntimeMethodSwap.swift +41 -0
- package/ios/Utility/ViewIdentifier.swift +37 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +88 -0
- package/lib/commonjs/index.js +1443 -0
- package/lib/commonjs/sdk/autoTracking.js +1087 -0
- package/lib/commonjs/sdk/constants.js +166 -0
- package/lib/commonjs/sdk/errorTracking.js +187 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +205 -0
- package/lib/commonjs/sdk/navigation.js +128 -0
- package/lib/commonjs/sdk/networkInterceptor.js +375 -0
- package/lib/commonjs/sdk/utils.js +433 -0
- package/lib/commonjs/sdk/version.js +13 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +83 -0
- package/lib/module/index.js +1341 -0
- package/lib/module/sdk/autoTracking.js +1059 -0
- package/lib/module/sdk/constants.js +154 -0
- package/lib/module/sdk/errorTracking.js +177 -0
- package/lib/module/sdk/index.js +26 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +120 -0
- package/lib/module/sdk/networkInterceptor.js +364 -0
- package/lib/module/sdk/utils.js +412 -0
- package/lib/module/sdk/version.js +7 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +160 -0
- package/lib/typescript/components/Mask.d.ts +54 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +226 -0
- package/lib/typescript/sdk/constants.d.ts +138 -0
- package/lib/typescript/sdk/errorTracking.d.ts +47 -0
- package/lib/typescript/sdk/index.d.ts +24 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
- package/lib/typescript/sdk/navigation.d.ts +48 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
- package/lib/typescript/sdk/utils.d.ts +193 -0
- package/lib/typescript/sdk/version.d.ts +6 -0
- package/lib/typescript/types/index.d.ts +618 -0
- package/package.json +122 -0
- package/rejourney.podspec +23 -0
- package/src/NativeRejourney.ts +185 -0
- package/src/components/Mask.tsx +93 -0
- package/src/index.ts +1555 -0
- package/src/sdk/autoTracking.ts +1245 -0
- package/src/sdk/constants.ts +155 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +25 -0
- package/src/sdk/metricsTracking.ts +227 -0
- package/src/sdk/navigation.ts +152 -0
- package/src/sdk/networkInterceptor.ts +423 -0
- package/src/sdk/utils.ts +442 -0
- package/src/sdk/version.ts +6 -0
- package/src/types/expo-router.d.ts +7 -0
- 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
|