Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@

- `app`: Kotlin and Jetpack Compose
- `example-java-xml`: Java activities and XML layouts

## Development

After intentionally changing the public API, regenerate the API baseline with `./gradlew :nubrick:apiDump` and verify it with `./gradlew :nubrick:apiCheck`.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ androidx-browser = { module = "androidx.browser:browser", version.ref = "browser
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }

coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
Expand Down
10 changes: 6 additions & 4 deletions nubrick/api/nubrick.api
Original file line number Diff line number Diff line change
Expand Up @@ -289,19 +289,21 @@ public final class app/nubrick/nubrick/data/StackFrame$Companion {

public final class app/nubrick/nubrick/data/TrackCrashEvent {
public static final field $stable I
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;)V
public synthetic fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;Ljava/time/ZonedDateTime;)V
public synthetic fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;Ljava/time/ZonedDateTime;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lapp/nubrick/nubrick/data/CrashSeverity;
public final fun copy (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;)Lapp/nubrick/nubrick/data/TrackCrashEvent;
public static synthetic fun copy$default (Lapp/nubrick/nubrick/data/TrackCrashEvent;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;ILjava/lang/Object;)Lapp/nubrick/nubrick/data/TrackCrashEvent;
public final fun component5 ()Ljava/time/ZonedDateTime;
public final fun copy (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;Ljava/time/ZonedDateTime;)Lapp/nubrick/nubrick/data/TrackCrashEvent;
public static synthetic fun copy$default (Lapp/nubrick/nubrick/data/TrackCrashEvent;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lapp/nubrick/nubrick/data/CrashSeverity;Ljava/time/ZonedDateTime;ILjava/lang/Object;)Lapp/nubrick/nubrick/data/TrackCrashEvent;
public fun equals (Ljava/lang/Object;)Z
public final fun getExceptions ()Ljava/util/List;
public final fun getFlutterSdkVersion ()Ljava/lang/String;
public final fun getPlatform ()Ljava/lang/String;
public final fun getSeverity ()Lapp/nubrick/nubrick/data/CrashSeverity;
public final fun getTimestamp ()Ljava/time/ZonedDateTime;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
Expand Down
1 change: 1 addition & 0 deletions nubrick/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.process)
testImplementation(libs.junit)
testImplementation(libs.mockito.core)
androidTestImplementation(platform(libs.compose.bom))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

@RunWith(AndroidJUnit4::class)
class InitializeContainmentAndroidTest {
Expand Down Expand Up @@ -78,6 +80,27 @@ class InitializeContainmentAndroidTest {
assertEquals("user-after-retry", NubrickSDK.getUserId())
}

@Test
fun initializeAndResetSucceedOffTheMainThread() {
val realContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
val executor = Executors.newSingleThreadExecutor()
try {
val initialized = executor.submit<Boolean> {
NubrickSDK.initialize(
context = realContext,
config = Config(projectId = "test-project-id"),
)
}.get(5, TimeUnit.SECONDS)
assertTrue(initialized)

executor.submit {
runBlocking { NubrickSDK.resetForTest() }
}.get(5, TimeUnit.SECONDS)
} finally {
executor.shutdownNow()
}
}

@Test
fun initializeReturnsTrueWhenAlreadyInitialized() {
val realContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package app.nubrick.nubrick.data.database

import android.database.sqlite.SQLiteDatabase
import app.nubrick.nubrick.schema.ConditionOperator
import app.nubrick.nubrick.schema.FrequencyUnit
import app.nubrick.nubrick.schema.UserEventFrequencyCondition
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import kotlinx.coroutines.runBlocking

class DatabaseRepositoryAndroidTest {
private lateinit var db: SQLiteDatabase
private lateinit var repository: DatabaseRepositoryImpl

@Before
fun setUp() {
db = SQLiteDatabase.create(null)
db.execSQL(SQL_CREATE_EXPERIMENT_HISTORY_TABLE)
db.execSQL(SQL_CREATE_USER_EVENT_TABLE)
db.execSQL(SQL_CREATE_TRACK_OUTBOX_TABLE)
repository = DatabaseRepositoryImpl(db)
}

@After
fun tearDown() {
db.close()
}

@Test
fun eventFrequencyConditionCountsEventsInSameBucket() = runBlocking {
repository.appendUserEvent("purchase")
repository.appendUserEvent("purchase")

val matched = repository.isMatchedToUserEventFrequencyCondition(
UserEventFrequencyCondition(
eventName = "purchase",
lookbackPeriod = 1,
unit = FrequencyUnit.DAY,
comparison = ConditionOperator.GreaterThanOrEqual,
threshold = 2,
)
)

Assert.assertTrue(matched)
}

@Test
fun trackOutboxKeepsFifoRecordsUntilAcknowledged() = runBlocking {
val outbox = TrackOutbox(databaseProvider = { db })
Assert.assertNotNull(outbox.insertEvent("old", createdAt = 1))
Assert.assertNotNull(outbox.insertEvent("new", createdAt = 2))

val batch = outbox.nextNormalBatch(maxEvents = 50, maxPayloadBytes = 512 * 1024)
Assert.assertEquals(listOf("old", "new"), batch.map { it.eventId })

outbox.remove(listOf("old"))
Assert.assertEquals(listOf("new"), outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId })
}

@Test
fun trackOutboxEvictsOnlyTheOldestEventAtCountLimit() = runBlocking {
val outbox = TrackOutbox(
databaseProvider = { db },
limits = TrackOutboxLimits(maxEventCount = 2, maxQueueBytes = 1_000_000),
)

outbox.insertEvent("first", createdAt = 1)
outbox.insertEvent("second", createdAt = 2)
outbox.insertEvent("third", createdAt = 3)

Assert.assertEquals(
listOf("second", "third"),
outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId },
)
}

@Test
fun trackOutboxEvictsOnlyTheOldestEventAtByteLimit() = runBlocking {
val firstPayload = "{\"eventUuid\":\"first\"}"
val secondPayload = "{\"eventUuid\":\"second\"}"
val outbox = TrackOutbox(
databaseProvider = { db },
limits = TrackOutboxLimits(
maxEventCount = 10,
maxQueueBytes = maxOf(firstPayload.length, secondPayload.length),
),
)

outbox.insertEvent("first", createdAt = 1, payload = firstPayload)
outbox.insertEvent("second", createdAt = 2, payload = secondPayload)

Assert.assertEquals(
listOf("second"),
outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId },
)
}

@Test
fun trackOutboxEvictsOldestEventRegardlessOfType() = runBlocking {
val outbox = TrackOutbox(
databaseProvider = { db },
limits = TrackOutboxLimits(maxEventCount = 2, maxQueueBytes = 1_000_000),
)

outbox.insertEvent("crash-1", createdAt = 1, eventType = "crash")
outbox.insertEvent("event-1", createdAt = 2)
outbox.insertEvent("event-2", createdAt = 3)

Assert.assertNull(outbox.nextCrash())
Assert.assertEquals(
listOf("event-1", "event-2"),
outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId },
)
}

@Test
fun trackOutboxInsertOfDuplicateEventIdIsIdempotent() = runBlocking {
val outbox = TrackOutbox(databaseProvider = { db })
val payload = "{\"eventUuid\":\"same\"}"

Assert.assertNotNull(outbox.insertEvent("same", createdAt = 1, payload = payload))
Assert.assertNotNull(outbox.insertEvent("same", createdAt = 2, payload = payload))
Assert.assertEquals(
listOf("same"),
outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId },
)
}

@Test
fun trackOutboxCreateTableIsIdempotent() {
db.execSQL(SQL_CREATE_TRACK_OUTBOX_TABLE)
}

@Test
fun trackOutboxKeepsUserIdentityWithTheEvent() = runBlocking {
val outbox = TrackOutbox(databaseProvider = { db })
outbox.insertEvent("event-a", createdAt = 1, userId = "user-a", meta = """{"appVersion":"1.0"}""")

val stored = outbox.nextNormalBatch(50, 512 * 1024).single()
Assert.assertEquals("user-a", stored.userId)
Assert.assertEquals("""{"appVersion":"1.0"}""", stored.meta)
}

@Test
fun trackOutboxDoesNotMixUsersInTheSameBatch() = runBlocking {
val outbox = TrackOutbox(databaseProvider = { db })
outbox.insertEvent("a1", createdAt = 1, userId = "user-a")
outbox.insertEvent("b1", createdAt = 2, userId = "user-b")
outbox.insertEvent("a2", createdAt = 3, userId = "user-a")

Assert.assertEquals(listOf("a1"), outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId })
outbox.remove(listOf("a1"))
Assert.assertEquals(listOf("b1"), outbox.nextNormalBatch(50, 512 * 1024).map { it.eventId })
}
}

private const val TEST_META = """{"platform":"android"}"""

private suspend fun TrackOutbox.insertEvent(
eventId: String,
createdAt: Long,
eventType: String = "event",
userId: String = "user-a",
payload: String = "{\"eventUuid\":\"$eventId\"}",
meta: String = TEST_META,
) = insert(eventId, payload, eventType, createdAt, userId, meta)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,29 @@ internal interface DatabaseRepository {
}

private const val DATABASE_NAME = "Nativebrik.sdk.db"
private const val DATABASE_VERSION = 1
private const val DATABASE_VERSION = 2
internal class NubrickDbHelper(context: Context): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
try {
db.execSQL(SQL_CREATE_EXPERIMENT_HISTORY_TABLE)
db.execSQL(SQL_CREATE_USER_EVENT_TABLE)
db.execSQL(SQL_CREATE_TRACK_OUTBOX_TABLE)
} catch (_: Exception) {
throw Exception("Nubrick SDK couldn't create a sqlite database.")
}
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
try {
if (oldVersion < 2) {
db.execSQL(SQL_CREATE_TRACK_OUTBOX_TABLE)
}
} catch (_: Exception) {
throw Exception("Nubrick SDK couldn't create a sqlite database.")
}
}

override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onUpgrade(db, oldVersion, newVersion)
}
}
Expand Down
Loading
Loading