diff --git a/android/app/src/main/java/com/nativebrik/example/MainActivity.kt b/android/app/src/main/java/com/nativebrik/example/MainActivity.kt index d4f9744..052d526 100644 --- a/android/app/src/main/java/com/nativebrik/example/MainActivity.kt +++ b/android/app/src/main/java/com/nativebrik/example/MainActivity.kt @@ -6,6 +6,8 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -25,9 +27,14 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) this.nativebrik = NativebrikClient( - config = Config(projectId = "ckto7v223akg00ag3jsg"), + config = Config(projectId = "cgv3p3223akg00fod19g"), context = this.applicationContext, ) + + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + this.nativebrik.experiment.record(throwable) + } + setContent { NativebrikAndroidTheme { NativebrikProvider(client = nativebrik) { @@ -36,11 +43,18 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - Column { - Greeting("Android") + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + Nativebrik.client.experiment.Embedding( + "HEADER_INFORMATION", + arguments = emptyMap(), + modifier = Modifier.height(100f.dp), + ) Nativebrik.client.experiment.Embedding( - "SCROLLABLE_CONTENT", - modifier = Modifier.height(400f.dp) + "TOP_COMPONENT", + arguments = emptyMap(), + modifier = Modifier.height(270f.dp), ) } } diff --git a/android/nativebrik/build.gradle.kts b/android/nativebrik/build.gradle.kts index d9841d1..5588d7e 100644 --- a/android/nativebrik/build.gradle.kts +++ b/android/nativebrik/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "com.nativebrik" -version = "0.1.3" +version = "0.1.4" android { namespace = "com.nativebrik.sdk" diff --git a/android/nativebrik/src/main/java/com/nativebrik/sdk/data/container.kt b/android/nativebrik/src/main/java/com/nativebrik/sdk/data/container.kt index 97b4dea..7075acc 100644 --- a/android/nativebrik/src/main/java/com/nativebrik/sdk/data/container.kt +++ b/android/nativebrik/src/main/java/com/nativebrik/sdk/data/container.kt @@ -38,6 +38,8 @@ internal interface Container { suspend fun fetchEmbedding(experimentId: String, componentId: String? = null): Result suspend fun fetchInAppMessage(trigger: String): Result suspend fun fetchRemoteConfig(experimentId: String): Result + + fun record(throwable: Throwable) } internal class ContainerImpl( @@ -201,4 +203,8 @@ internal class ContainerImpl( return Result.success(experimentId to variant) } + override fun record(throwable: Throwable) { + this.trackRepository.record(throwable) + } + } diff --git a/android/nativebrik/src/main/java/com/nativebrik/sdk/data/track.kt b/android/nativebrik/src/main/java/com/nativebrik/sdk/data/track.kt index b5fe8e4..ce21ef6 100644 --- a/android/nativebrik/src/main/java/com/nativebrik/sdk/data/track.kt +++ b/android/nativebrik/src/main/java/com/nativebrik/sdk/data/track.kt @@ -4,20 +4,58 @@ import com.nativebrik.sdk.Config import com.nativebrik.sdk.data.user.NativebrikUser import com.nativebrik.sdk.data.user.formatISO8601 import com.nativebrik.sdk.data.user.getCurrentDate +import com.nativebrik.sdk.schema.ListDecoder +import com.nativebrik.sdk.schema.StringDecoder +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject import okio.withLock import java.time.ZonedDateTime import java.util.Timer import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.fixedRateTimer +private val CRASH_RECORD_KEY = "CRASH_RECORD_KEY" + +data class CrashRecord( + val reason: String?, + val callStacks: List? +) { + fun encode(): JsonObject { + val callStacks = this.callStacks?.map { + JsonPrimitive(it) + } ?: emptyList() + return JsonObject(mapOf( + "reason" to JsonPrimitive(this.reason), + "callStacks" to JsonArray(callStacks) + )) + } + + companion object { + fun decode(element: JsonElement?): CrashRecord? { + if (element == null) return null + if (element !is JsonObject) return null + + return CrashRecord( + reason = StringDecoder.decode(element.jsonObject["name"]), + callStacks = ListDecoder.decode(element.jsonObject["callStacks"]) { + StringDecoder.decode( + it + ) + }, + ) + } + } +} + + internal data class TrackUserEvent( val name: String, val timestamp: ZonedDateTime = getCurrentDate(), @@ -78,6 +116,8 @@ internal data class TrackRequest( internal interface TrackRepository { fun trackExperimentEvent(event: TrackExperimentEvent) fun trackEvent(event: TrackUserEvent) + + fun record(throwable: Throwable) } internal class TrackRepositoryImpl: TrackRepository { @@ -92,6 +132,8 @@ internal class TrackRepositoryImpl: TrackRepository { internal constructor(config: Config, user: NativebrikUser) { this.config = config this.user = user + + this.report() } override fun trackEvent(event: TrackUserEvent) { @@ -106,10 +148,10 @@ internal class TrackRepositoryImpl: TrackRepository { this.queueLock.withLock { if (this.timer == null) { val self = this - GlobalScope.launch(Dispatchers.Main) { + CoroutineScope(Dispatchers.Main).launch { self.timer?.cancel() self.timer = fixedRateTimer(initialDelay = 0, period = 4000) { - GlobalScope.launch(Dispatchers.IO) { + CoroutineScope(Dispatchers.IO).launch { self.sendAndFlush() } } @@ -117,7 +159,7 @@ internal class TrackRepositoryImpl: TrackRepository { } if (this.buffer.size >= this.maxBatchSize) { val self = this - GlobalScope.launch(Dispatchers.IO) { + CoroutineScope(Dispatchers.IO).launch { self.sendAndFlush() } } @@ -144,4 +186,41 @@ internal class TrackRepositoryImpl: TrackRepository { this.buffer.addAll(tempBuffer) } } + + private fun report() { + val data = this.user.preferences?.getString(CRASH_RECORD_KEY, "") ?: "" + if (data.isEmpty()) { + return + } + this.user.preferences?.edit()?.remove(CRASH_RECORD_KEY)?.apply() + + val json = Json.decodeFromString(data) + val crashRecord = CrashRecord.decode(json) ?: return + val causedByNativebrik = crashRecord.callStacks?.any { it.contains("com.nativebrik.sdk") } == true || + crashRecord.reason?.contains("com.nativebrik.sdk") == true + + this.buffer.add(TrackEvent.UserEvent(TrackUserEvent( + name = "N_CRASH_RECORD" + ))) + + if (causedByNativebrik) { + buffer.add(TrackEvent.UserEvent(TrackUserEvent( + name = "N_CRASH_IN_SDK_RECORD" + ))) + } + + val self = this + CoroutineScope(Dispatchers.IO).launch { + self.sendAndFlush() + } + } + + override fun record(throwable: Throwable) { + val record = CrashRecord( + reason = throwable.message, + callStacks = throwable.stackTrace.map { it.toString() } + ) + val data = Json.encodeToString(record.encode()) + this.user.preferences?.edit()?.putString(CRASH_RECORD_KEY, data)?.apply() + } } diff --git a/android/nativebrik/src/main/java/com/nativebrik/sdk/data/user/user.kt b/android/nativebrik/src/main/java/com/nativebrik/sdk/data/user/user.kt index 4463ef4..6403cd5 100644 --- a/android/nativebrik/src/main/java/com/nativebrik/sdk/data/user/user.kt +++ b/android/nativebrik/src/main/java/com/nativebrik/sdk/data/user/user.kt @@ -51,7 +51,7 @@ internal data class UserProperty( class NativebrikUser { private var properties: MutableMap = mutableMapOf() - private var preferences: SharedPreferences? = null + internal var preferences: SharedPreferences? = null private var lastBootTime: ZonedDateTime = getCurrentDate() val id: String diff --git a/android/nativebrik/src/main/java/com/nativebrik/sdk/sdk.kt b/android/nativebrik/src/main/java/com/nativebrik/sdk/sdk.kt index d5fed3d..023f6ef 100644 --- a/android/nativebrik/src/main/java/com/nativebrik/sdk/sdk.kt +++ b/android/nativebrik/src/main/java/com/nativebrik/sdk/sdk.kt @@ -24,7 +24,7 @@ import com.nativebrik.sdk.data.user.NativebrikUser import com.nativebrik.sdk.remoteconfig.RemoteConfigLoadingState import com.nativebrik.sdk.schema.UIBlock -const val VERSION = "0.1.3" +const val VERSION = "0.1.4" data class Endpoint( val cdn: String = "https://cdn.nativebrik.com", @@ -135,6 +135,10 @@ public class NativebrikExperiment { this.trigger.dispatch(event) } + public fun record(throwable: Throwable) { + this.container.record(throwable) + } + @Composable public fun Overlay() { Trigger(trigger = this.trigger)