@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,290 @@
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
+ package com.rejourney.engine
18
+
19
+ import android.content.Context
20
+ import android.content.SharedPreferences
21
+ import android.os.Build
22
+ import android.provider.Settings
23
+ import kotlinx.coroutines.*
24
+ import okhttp3.*
25
+ import okhttp3.MediaType.Companion.toMediaType
26
+ import okhttp3.RequestBody.Companion.toRequestBody
27
+ import org.json.JSONObject
28
+ import java.security.MessageDigest
29
+ import java.util.concurrent.TimeUnit
30
+
31
+ /**
32
+ * Establishes device identity and obtains upload credentials
33
+ * Android implementation aligned with iOS DeviceRegistrar.swift
34
+ */
35
+ class DeviceRegistrar private constructor(private val context: Context) {
36
+
37
+ companion object {
38
+ @Volatile
39
+ private var instance: DeviceRegistrar? = null
40
+
41
+ fun getInstance(context: Context): DeviceRegistrar {
42
+ return instance ?: synchronized(this) {
43
+ instance ?: DeviceRegistrar(context.applicationContext).also { instance = it }
44
+ }
45
+ }
46
+
47
+ // For static access pattern matching iOS
48
+ val shared: DeviceRegistrar?
49
+ get() = instance
50
+ }
51
+
52
+ // Public Configuration
53
+ var endpoint: String = "https://api.rejourney.co"
54
+ var apiToken: String? = null
55
+
56
+ // Public State
57
+ var deviceFingerprint: String? = null
58
+ private set
59
+ var uploadCredential: String? = null
60
+ private set
61
+ var credentialValid: Boolean = false
62
+ private set
63
+
64
+ // Private State
65
+ private val prefsKey = "com.rejourney.device"
66
+ private val fingerprintKey = "device_fingerprint"
67
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
68
+
69
+ private val httpClient: OkHttpClient = OkHttpClient.Builder()
70
+ .connectTimeout(5, TimeUnit.SECONDS) // Short timeout for debugging
71
+ .readTimeout(10, TimeUnit.SECONDS)
72
+ .writeTimeout(10, TimeUnit.SECONDS)
73
+ .build()
74
+
75
+ init {
76
+ establishIdentity()
77
+ }
78
+
79
+ // Credential Management
80
+
81
+ fun obtainCredential(apiToken: String, callback: (Boolean, String?) -> Unit) {
82
+ DiagnosticLog.notice("[DeviceRegistrar] ★★★ obtainCredential v2 ★★★")
83
+ DiagnosticLog.notice("[DeviceRegistrar] obtainCredential called, apiToken=${apiToken.take(12)}...")
84
+ this.apiToken = apiToken
85
+
86
+ val fingerprint = deviceFingerprint
87
+ if (fingerprint == null) {
88
+ DiagnosticLog.caution("[DeviceRegistrar] No fingerprint available!")
89
+ callback(false, "Device identity unavailable")
90
+ return
91
+ }
92
+ DiagnosticLog.notice("[DeviceRegistrar] Fingerprint OK, fetching credential from server")
93
+
94
+ scope.launch {
95
+ fetchServerCredential(fingerprint, apiToken, callback)
96
+ }
97
+ }
98
+
99
+ fun invalidateCredential() {
100
+ uploadCredential = null
101
+ credentialValid = false
102
+ }
103
+
104
+ // Device Profile
105
+
106
+ fun gatherDeviceProfile(): Map<String, Any> {
107
+ val displayMetrics = context.resources.displayMetrics
108
+ val packageInfo = try {
109
+ context.packageManager.getPackageInfo(context.packageName, 0)
110
+ } catch (e: Exception) {
111
+ null
112
+ }
113
+
114
+ return mapOf(
115
+ "fingerprint" to (deviceFingerprint ?: ""),
116
+ "os" to "android",
117
+ "hwModel" to Build.MODEL,
118
+ "osRelease" to Build.VERSION.RELEASE,
119
+ "sdkInt" to Build.VERSION.SDK_INT,
120
+ "appRelease" to (packageInfo?.versionName ?: "unknown"),
121
+ "buildId" to (packageInfo?.let {
122
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) it.longVersionCode.toString()
123
+ else @Suppress("DEPRECATION") it.versionCode.toString()
124
+ } ?: "unknown"),
125
+ "displayWidth" to displayMetrics.widthPixels,
126
+ "displayHeight" to displayMetrics.heightPixels,
127
+ "displayDensity" to displayMetrics.density,
128
+ "region" to java.util.Locale.getDefault().toString(),
129
+ "tz" to java.util.TimeZone.getDefault().id,
130
+ "manufacturer" to Build.MANUFACTURER,
131
+ "brand" to Build.BRAND,
132
+ "device" to Build.DEVICE,
133
+ "simulated" to isEmulator()
134
+ )
135
+ }
136
+
137
+ fun composeAuthHeaders(): Map<String, String> {
138
+ val headers = mutableMapOf<String, String>()
139
+
140
+ apiToken?.let { token ->
141
+ headers["x-rejourney-key"] = token
142
+ }
143
+ uploadCredential?.let { cred ->
144
+ headers["x-upload-token"] = cred
145
+ }
146
+
147
+ return headers
148
+ }
149
+
150
+ // Identity Establishment
151
+
152
+ private fun establishIdentity() {
153
+ val prefs = context.getSharedPreferences(prefsKey, Context.MODE_PRIVATE)
154
+ val stored = prefs.getString(fingerprintKey, null)
155
+
156
+ if (stored != null) {
157
+ deviceFingerprint = stored
158
+ return
159
+ }
160
+
161
+ val fresh = generateFingerprint()
162
+ deviceFingerprint = fresh
163
+ prefs.edit().putString(fingerprintKey, fresh).apply()
164
+ }
165
+
166
+ private fun generateFingerprint(): String {
167
+ val packageName = context.packageName
168
+
169
+ var composite = packageName
170
+ composite += Build.MODEL
171
+ composite += Build.MANUFACTURER
172
+ composite += Build.VERSION.RELEASE
173
+ composite += getAndroidId()
174
+
175
+ return sha256(composite)
176
+ }
177
+
178
+ private fun getAndroidId(): String {
179
+ return try {
180
+ Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: java.util.UUID.randomUUID().toString()
181
+ } catch (e: Exception) {
182
+ java.util.UUID.randomUUID().toString()
183
+ }
184
+ }
185
+
186
+ // Server Communication
187
+
188
+ private suspend fun fetchServerCredential(
189
+ fingerprint: String,
190
+ apiToken: String,
191
+ callback: (Boolean, String?) -> Unit
192
+ ) {
193
+ val requestStartTime = System.currentTimeMillis()
194
+ DiagnosticLog.notice("[DeviceRegistrar] fetchServerCredential: starting request to $endpoint")
195
+ DiagnosticLog.debugCredentialFlow("START", fingerprint, true, "apiToken=${apiToken.take(12)}...")
196
+
197
+ val url = "$endpoint/api/ingest/auth/device"
198
+
199
+ val profile = gatherDeviceProfile()
200
+ val payload = JSONObject().apply {
201
+ put("deviceId", fingerprint)
202
+ put("metadata", JSONObject(profile))
203
+ }
204
+
205
+ DiagnosticLog.debugNetworkRequest("POST", url, mapOf("x-rejourney-key" to apiToken))
206
+
207
+ val requestBody = payload.toString().toRequestBody("application/json".toMediaType())
208
+
209
+ val request = Request.Builder()
210
+ .url(url)
211
+ .post(requestBody)
212
+ .header("Content-Type", "application/json")
213
+ .header("x-rejourney-key", apiToken)
214
+ .build()
215
+
216
+ try {
217
+ val response = httpClient.newCall(request).execute()
218
+ val durationMs = System.currentTimeMillis() - requestStartTime
219
+ val body = response.body?.string()
220
+
221
+ DiagnosticLog.notice("[DeviceRegistrar] Response: code=${response.code}, bodyLen=${body?.length ?: 0}, duration=${durationMs}ms")
222
+ DiagnosticLog.debugNetworkResponse(url, response.code, body?.length ?: 0, durationMs.toDouble())
223
+
224
+ if (response.isSuccessful && body != null) {
225
+ try {
226
+ val json = JSONObject(body)
227
+ val token = json.optString("uploadToken", null)
228
+ if (token != null) {
229
+ DiagnosticLog.notice("[DeviceRegistrar] Got uploadToken from server")
230
+ DiagnosticLog.debugCredentialFlow("SUCCESS", fingerprint, true, "Got server credential uploadToken=${token.take(12)}...")
231
+ uploadCredential = token
232
+ credentialValid = true
233
+ withContext(Dispatchers.Main) { callback(true, token) }
234
+ return
235
+ }
236
+ } catch (e: Exception) {
237
+ DiagnosticLog.notice("[DeviceRegistrar] JSON parse error: ${e.message}")
238
+ DiagnosticLog.debugCredentialFlow("PARSE_ERROR", fingerprint, false, e.message ?: "")
239
+ }
240
+ } else {
241
+ val bodyPreview = body?.take(200) ?: "empty"
242
+ DiagnosticLog.notice("[DeviceRegistrar] Server error: ${response.code}")
243
+ DiagnosticLog.debugCredentialFlow("HTTP_ERROR", fingerprint, false, "status=${response.code} body=$bodyPreview")
244
+ }
245
+
246
+ // Fallback to local credential
247
+ DiagnosticLog.notice("[DeviceRegistrar] Using local fallback credential")
248
+ DiagnosticLog.debugCredentialFlow("FALLBACK", fingerprint, true, "Using local credential after server error")
249
+ uploadCredential = synthesizeLocalCredential(fingerprint, apiToken)
250
+ credentialValid = true
251
+ withContext(Dispatchers.Main) { callback(true, uploadCredential) }
252
+
253
+ } catch (e: Exception) {
254
+ val durationMs = System.currentTimeMillis() - requestStartTime
255
+ DiagnosticLog.notice("[DeviceRegistrar] Network exception: ${e.message}, using fallback")
256
+ DiagnosticLog.debugCredentialFlow("FALLBACK", fingerprint, true, "No response, using local credential error=${e.message}")
257
+ DiagnosticLog.debugNetworkResponse(url, 0, 0, durationMs.toDouble())
258
+
259
+ uploadCredential = synthesizeLocalCredential(fingerprint, apiToken)
260
+ credentialValid = true
261
+ withContext(Dispatchers.Main) { callback(true, uploadCredential) }
262
+ }
263
+ }
264
+
265
+ private fun synthesizeLocalCredential(fingerprint: String, apiToken: String): String {
266
+ val timestamp = System.currentTimeMillis() / 1000
267
+ val composite = "$apiToken:$fingerprint:$timestamp"
268
+ return sha256(composite)
269
+ }
270
+
271
+ // Hardware Detection
272
+
273
+ private fun isEmulator(): Boolean {
274
+ return (Build.FINGERPRINT.startsWith("generic")
275
+ || Build.FINGERPRINT.startsWith("unknown")
276
+ || Build.MODEL.contains("google_sdk")
277
+ || Build.MODEL.contains("Emulator")
278
+ || Build.MODEL.contains("Android SDK built for x86")
279
+ || Build.MANUFACTURER.contains("Genymotion")
280
+ || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
281
+ || "google_sdk" == Build.PRODUCT)
282
+ }
283
+
284
+ // Cryptographic Helpers
285
+
286
+ private fun sha256(input: String): String {
287
+ val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
288
+ return bytes.joinToString("") { "%02x".format(it) }
289
+ }
290
+ }
@@ -0,0 +1,385 @@
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
+ package com.rejourney.engine
18
+
19
+ import android.os.Debug
20
+ import android.os.SystemClock
21
+ import android.util.Log
22
+ import java.text.SimpleDateFormat
23
+ import java.util.*
24
+
25
+ /**
26
+ * Severity tiers for SDK diagnostic messages
27
+ */
28
+ enum class LogLevel(val value: Int) {
29
+ TRACE(0),
30
+ NOTICE(1),
31
+ CAUTION(2),
32
+ FAULT(3)
33
+ }
34
+
35
+ /**
36
+ * Captures point-in-time performance metrics
37
+ */
38
+ data class PerformanceSnapshot(
39
+ val wallTimeMs: Double,
40
+ val cpuTimeMs: Double,
41
+ val mainThreadTimeMs: Double,
42
+ val timestamp: Long,
43
+ val isMainThread: Boolean,
44
+ val threadName: String
45
+ ) {
46
+ companion object {
47
+ fun capture(): PerformanceSnapshot {
48
+ val isMain = Thread.currentThread() == android.os.Looper.getMainLooper().thread
49
+ val threadName = if (isMain) {
50
+ "main"
51
+ } else {
52
+ Thread.currentThread().name.ifEmpty {
53
+ "bg-${Thread.currentThread().id.toString(16).takeLast(4)}"
54
+ }
55
+ }
56
+
57
+ return PerformanceSnapshot(
58
+ wallTimeMs = SystemClock.elapsedRealtime().toDouble(),
59
+ cpuTimeMs = Debug.threadCpuTimeNanos() / 1_000_000.0,
60
+ mainThreadTimeMs = if (isMain) SystemClock.elapsedRealtime().toDouble() else 0.0,
61
+ timestamp = System.currentTimeMillis(),
62
+ isMainThread = isMain,
63
+ threadName = threadName
64
+ )
65
+ }
66
+ }
67
+
68
+ fun elapsed(since: PerformanceSnapshot): Triple<Double, Double, String> {
69
+ return Triple(
70
+ wallTimeMs - since.wallTimeMs,
71
+ cpuTimeMs - since.cpuTimeMs,
72
+ if (isMainThread) "🟢 MAIN" else "🔵 BG($threadName)"
73
+ )
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Centralized logging facility for SDK diagnostics
79
+ * Android implementation aligned with iOS DiagnosticLog.swift
80
+ */
81
+ object DiagnosticLog {
82
+
83
+ private const val TAG = "RJ"
84
+
85
+ // Configuration
86
+ @JvmStatic var minimumLevel: Int = 1
87
+ @JvmStatic var includeTimestamp: Boolean = true
88
+ @JvmStatic var detailedOutput: Boolean = false
89
+ @JvmStatic var performanceTracing: Boolean = false
90
+
91
+ private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US)
92
+
93
+ // Level-Based Emission
94
+
95
+ @JvmStatic
96
+ fun emit(level: LogLevel, message: String) {
97
+ if (level.value < minimumLevel) return
98
+
99
+ val prefix = when (level) {
100
+ LogLevel.TRACE -> "TRACE"
101
+ LogLevel.NOTICE -> "INFO"
102
+ LogLevel.CAUTION -> "WARN"
103
+ LogLevel.FAULT -> "ERROR"
104
+ }
105
+
106
+ writeLog(prefix, message)
107
+ }
108
+
109
+ // Convenience Methods
110
+
111
+ @JvmStatic
112
+ fun trace(message: String) {
113
+ if (minimumLevel > 0) return
114
+ writeLog("VERBOSE", message)
115
+ }
116
+
117
+ @JvmStatic
118
+ fun notice(message: String) {
119
+ if (minimumLevel > 1) return
120
+ writeLog("INFO", message)
121
+ }
122
+
123
+ @JvmStatic
124
+ fun caution(message: String) {
125
+ if (minimumLevel > 2) return
126
+ writeLog("WARN", message)
127
+ }
128
+
129
+ @JvmStatic
130
+ fun fault(message: String) {
131
+ if (minimumLevel > 3) return
132
+ writeLog("ERROR", message)
133
+ }
134
+
135
+ // Lifecycle Events
136
+
137
+ @JvmStatic
138
+ fun sdkReady(version: String) {
139
+ notice("[Rejourney] SDK initialized v$version")
140
+ }
141
+
142
+ @JvmStatic
143
+ fun sdkFailed(reason: String) {
144
+ fault("[Rejourney] Initialization failed: $reason")
145
+ }
146
+
147
+ @JvmStatic
148
+ fun replayBegan(sessionId: String) {
149
+ notice("[Rejourney] Recording started: $sessionId")
150
+ }
151
+
152
+ @JvmStatic
153
+ fun replayEnded(sessionId: String) {
154
+ notice("[Rejourney] Recording ended: $sessionId")
155
+ }
156
+
157
+ // Debug-Only Session Logs
158
+
159
+ @JvmStatic
160
+ fun debugSessionCreate(phase: String, details: String, perf: PerformanceSnapshot? = null) {
161
+ if (!detailedOutput) return
162
+ var msg = "📍 [SESSION] $phase: $details"
163
+ if (perf != null && performanceTracing) {
164
+ val threadIcon = if (perf.isMainThread) "🟢 MAIN" else "🔵 BG"
165
+ msg += " | $threadIcon wall=${"%.2f".format(perf.wallTimeMs)}ms cpu=${"%.2f".format(perf.cpuTimeMs)}ms"
166
+ }
167
+ writeLog("DEBUG", msg)
168
+ }
169
+
170
+ @JvmStatic
171
+ fun debugSessionTiming(operation: String, startPerf: PerformanceSnapshot, endPerf: PerformanceSnapshot) {
172
+ if (!detailedOutput || !performanceTracing) return
173
+ val (wall, cpu, thread) = endPerf.elapsed(startPerf)
174
+ writeLog("PERF", "⏱️ [$operation] $thread | wall=${"%.2f".format(wall)}ms cpu=${"%.2f".format(cpu)}ms")
175
+ }
176
+
177
+ // Enhanced Performance Logging
178
+
179
+ @JvmStatic
180
+ inline fun perfOperation(name: String, category: String = "OP", block: () -> Unit) {
181
+ if (!detailedOutput || !performanceTracing) {
182
+ block()
183
+ return
184
+ }
185
+
186
+ val start = PerformanceSnapshot.capture()
187
+ block()
188
+ val end = PerformanceSnapshot.capture()
189
+ val (wall, cpu, thread) = end.elapsed(start)
190
+
191
+ val warningThreshold = 16.67 // One frame at 60fps
192
+ val icon = when {
193
+ wall > warningThreshold -> "🔴"
194
+ wall > 8 -> "🟡"
195
+ else -> "🟢"
196
+ }
197
+
198
+ writeLog("PERF", "$icon [$category] $name | $thread | ⏱️ ${"%.2f".format(wall)}ms wall, ${"%.2f".format(cpu)}ms cpu")
199
+ }
200
+
201
+ @JvmStatic
202
+ fun perfStart(name: String, category: String = "ASYNC"): Long {
203
+ val start = System.currentTimeMillis()
204
+ if (detailedOutput && performanceTracing) {
205
+ val threadInfo = if (Thread.currentThread() == android.os.Looper.getMainLooper().thread) "🟢 MAIN" else "🔵 BG"
206
+ writeLog("PERF", "▶️ [$category] $name started | $threadInfo")
207
+ }
208
+ return start
209
+ }
210
+
211
+ @JvmStatic
212
+ fun perfEnd(name: String, startTime: Long, category: String = "ASYNC", success: Boolean = true) {
213
+ if (!detailedOutput || !performanceTracing) return
214
+ val elapsed = System.currentTimeMillis() - startTime
215
+ val threadInfo = if (Thread.currentThread() == android.os.Looper.getMainLooper().thread) "🟢 MAIN" else "🔵 BG"
216
+ val icon = if (success) "✅" else "❌"
217
+
218
+ val warningThreshold = 100.0 // 100ms for async ops
219
+ val timeIcon = when {
220
+ elapsed > warningThreshold -> "🔴"
221
+ elapsed > 50 -> "🟡"
222
+ else -> "🟢"
223
+ }
224
+
225
+ writeLog("PERF", "$icon [$category] $name finished | $threadInfo | $timeIcon ${elapsed}ms")
226
+ }
227
+
228
+ @JvmStatic
229
+ fun perfFrame(operation: String, durationMs: Double, frameNumber: Int, isMainThread: Boolean) {
230
+ if (!detailedOutput || !performanceTracing) return
231
+ val threadInfo = if (isMainThread) "🟢 MAIN" else "🔵 BG"
232
+ val budget = 33.33 // 30fps budget
233
+ val icon = when {
234
+ durationMs > budget -> "🔴 DROPPED"
235
+ durationMs > 16.67 -> "🟡 SLOW"
236
+ else -> "🟢 OK"
237
+ }
238
+
239
+ writeLog("FRAME", "🎬 [$operation] #$frameNumber | $threadInfo | $icon ${"%.2f".format(durationMs)}ms (budget: ${"%.1f".format(budget)}ms)")
240
+ }
241
+
242
+ @JvmStatic
243
+ fun perfBatch(operation: String, itemCount: Int, totalMs: Double, isMainThread: Boolean) {
244
+ if (!detailedOutput || !performanceTracing) return
245
+ val threadInfo = if (isMainThread) "🟢 MAIN" else "🔵 BG"
246
+ val avgMs = if (itemCount > 0) totalMs / itemCount else 0.0
247
+
248
+ writeLog("BATCH", "📦 [$operation] $itemCount items | $threadInfo | total=${"%.2f".format(totalMs)}ms avg=${"%.3f".format(avgMs)}ms/item")
249
+ }
250
+
251
+ @JvmStatic
252
+ fun perfNetwork(operation: String, url: String, durationMs: Double, bytesTransferred: Int, success: Boolean) {
253
+ if (!detailedOutput || !performanceTracing) return
254
+ val threadInfo = if (Thread.currentThread() == android.os.Looper.getMainLooper().thread) "🟢 MAIN" else "🔵 BG"
255
+ val throughputKBps = if (durationMs > 0) bytesTransferred / durationMs else 0.0
256
+ val icon = if (success) "✅" else "❌"
257
+ val shortUrl = url.split("/").takeLast(2).joinToString("/")
258
+
259
+ writeLog("NET", "$icon [$operation] $shortUrl | $threadInfo | ${"%.2f".format(durationMs)}ms, ${bytesTransferred}B @ ${"%.1f".format(throughputKBps)}KB/s")
260
+ }
261
+
262
+ // Debug-Only Presign Logs
263
+
264
+ @JvmStatic
265
+ fun debugPresignRequest(url: String, sessionId: String, kind: String, sizeBytes: Int) {
266
+ if (!detailedOutput) return
267
+ writeLog("DEBUG", "🔐 [PRESIGN-REQ] url=$url sessionId=$sessionId kind=$kind size=${sizeBytes}B")
268
+ }
269
+
270
+ @JvmStatic
271
+ fun debugPresignResponse(status: Int, segmentId: String?, uploadUrl: String?, durationMs: Double) {
272
+ if (!detailedOutput) return
273
+ if (segmentId != null && uploadUrl != null) {
274
+ val truncUrl = if (uploadUrl.length > 80) uploadUrl.take(80) + "..." else uploadUrl
275
+ writeLog("DEBUG", "✅ [PRESIGN-OK] status=$status segmentId=$segmentId uploadUrl=$truncUrl took=${"%.1f".format(durationMs)}ms")
276
+ } else {
277
+ writeLog("DEBUG", "❌ [PRESIGN-FAIL] status=$status took=${"%.1f".format(durationMs)}ms")
278
+ }
279
+ }
280
+
281
+ @JvmStatic
282
+ fun debugUploadProgress(phase: String, segmentId: String, bytesWritten: Long, totalBytes: Long) {
283
+ if (!detailedOutput) return
284
+ val pct = if (totalBytes > 0) bytesWritten.toDouble() / totalBytes * 100 else 0.0
285
+ writeLog("DEBUG", "📤 [UPLOAD] $phase segmentId=$segmentId progress=${"%.1f".format(pct)}% ($bytesWritten/${totalBytes}B)")
286
+ }
287
+
288
+ @JvmStatic
289
+ fun debugUploadComplete(segmentId: String, status: Int, durationMs: Double, throughputKBps: Double) {
290
+ if (!detailedOutput) return
291
+ writeLog("DEBUG", "📤 [UPLOAD-DONE] segmentId=$segmentId status=$status took=${"%.1f".format(durationMs)}ms throughput=${"%.1f".format(throughputKBps)}KB/s")
292
+ }
293
+
294
+ // Debug-Only Network Logs
295
+
296
+ @JvmStatic
297
+ fun debugNetworkRequest(method: String, url: String, headers: Map<String, String>?) {
298
+ if (!detailedOutput) return
299
+ var msg = "🌐 [NET-REQ] $method $url"
300
+ if (headers != null) {
301
+ val sanitized = headers.mapValues { if (it.value.length > 20) it.value.take(8) + "..." else it.value }
302
+ msg += " headers=$sanitized"
303
+ }
304
+ writeLog("DEBUG", msg)
305
+ }
306
+
307
+ @JvmStatic
308
+ fun debugNetworkResponse(url: String, status: Int, bodySize: Int, durationMs: Double) {
309
+ if (!detailedOutput) return
310
+ val shortUrl = url.split("/").lastOrNull() ?: url
311
+ writeLog("DEBUG", "🌐 [NET-RSP] $shortUrl status=$status size=${bodySize}B took=${"%.1f".format(durationMs)}ms")
312
+ }
313
+
314
+ // Debug-Only Credential Logs
315
+
316
+ @JvmStatic
317
+ fun debugCredentialFlow(phase: String, fingerprint: String?, success: Boolean, detail: String = "") {
318
+ if (!detailedOutput) return
319
+ val fp = fingerprint?.take(12)?.plus("...") ?: "nil"
320
+ val icon = if (success) "✅" else "❌"
321
+ writeLog("DEBUG", "$icon [CRED] $phase fingerprint=$fp $detail")
322
+ }
323
+
324
+ // Debug-Only Storage Logs
325
+
326
+ @JvmStatic
327
+ fun debugStorage(op: String, key: String, success: Boolean, detail: String = "") {
328
+ if (!detailedOutput) return
329
+ val icon = if (success) "✅" else "❌"
330
+ writeLog("DEBUG", "$icon [STORAGE] $op key=$key $detail")
331
+ }
332
+
333
+ // Debug-Only Memory Logs
334
+
335
+ @JvmStatic
336
+ fun debugMemoryUsage(context: String) {
337
+ if (!detailedOutput || !performanceTracing) return
338
+ val runtime = Runtime.getRuntime()
339
+ val usedMB = (runtime.totalMemory() - runtime.freeMemory()) / 1_048_576.0
340
+ val maxMB = runtime.maxMemory() / 1_048_576.0
341
+ val warningIcon = when {
342
+ usedMB > maxMB * 0.8 -> "🔴"
343
+ usedMB > maxMB * 0.5 -> "🟡"
344
+ else -> "🟢"
345
+ }
346
+ writeLog("MEM", "$warningIcon [$context] used=${"%.1f".format(usedMB)}MB max=${"%.1f".format(maxMB)}MB")
347
+ }
348
+
349
+ // Configuration
350
+
351
+ @JvmStatic
352
+ fun setVerbose(enabled: Boolean) {
353
+ detailedOutput = enabled
354
+ performanceTracing = enabled
355
+ minimumLevel = if (enabled) 0 else 1
356
+ if (enabled) {
357
+ writeLog("INFO", "🔧 [CONFIG] Debug mode ENABLED: detailedOutput=$detailedOutput, performanceTracing=$performanceTracing, minimumLevel=$minimumLevel")
358
+ }
359
+ }
360
+
361
+ // Internal Implementation - @PublishedApi allows inline functions to access
362
+
363
+ @PublishedApi
364
+ internal fun writeLog(prefix: String, message: String) {
365
+ val output = buildString {
366
+ append("[RJ]")
367
+ if (includeTimestamp) {
368
+ append(" ")
369
+ append(dateFormatter.format(Date()))
370
+ }
371
+ append(" [$prefix] $message")
372
+ }
373
+
374
+ when (prefix) {
375
+ "ERROR" -> Log.e(TAG, output)
376
+ "WARN" -> Log.w(TAG, output)
377
+ "INFO", "NOTICE" -> Log.i(TAG, output)
378
+ "DEBUG", "PERF", "FRAME", "BATCH", "NET", "MEM" -> Log.d(TAG, output)
379
+ else -> Log.v(TAG, output)
380
+ }
381
+ }
382
+ }
383
+
384
+ // Type alias for backward compatibility
385
+ typealias Logger = DiagnosticLog