@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,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
|