Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(sdk): swap out sharedpreferences #11

Merged
merged 23 commits into from
Aug 9, 2024
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
5 changes: 4 additions & 1 deletion TopsortAnalytics/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,17 @@ dependencies {
implementation 'androidx.core:core-ktx:1.13.1'

//Work Manager
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.datastore:datastore-preferences:1.1.1'

//JodaTime
implementation group: 'joda-time', name: 'joda-time', version: '2.12.5'

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20200518'
testImplementation 'org.assertj:assertj-core:3.26.0'
androidTestImplementation 'org.assertj:assertj-core:3.26.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.work:work-testing:2.9.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package com.topsort.analytics

import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.impl.utils.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.topsort.analytics.core.Logger
import com.topsort.analytics.model.Click
import com.topsort.analytics.model.ClickEvent
import com.topsort.analytics.model.Event
import com.topsort.analytics.model.Impression
import com.topsort.analytics.model.ImpressionEvent
import com.topsort.analytics.model.Purchase
import com.topsort.analytics.model.PurchaseEvent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.assertj.core.api.Assertions.assertThat
import org.json.JSONArray
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class EventPipelineTest {

private lateinit var workManager: WorkManager

@Before
fun setup() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext

val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()

// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(appContext, config)

workManager = WorkManager.getInstance(appContext)

EventPipeline.setup(appContext)
Logger.log.clear()
}

@Test
fun impressions_are_batched() {
val impressions1 = listOf(
getImpressionPromoted(),
getImpressionOrganic()
)
val impressions2 = listOf(
getImpressionPromoted(),
getImpressionOrganic()
)

runBlocking {
EventPipeline.clear()
val job1 = EventPipeline.storeImpression(
ImpressionEvent(impressions1),
shouldFlush = false
)
val job2 = EventPipeline.storeImpression(
ImpressionEvent(impressions2),
shouldFlush = false
)

// Make sure they're persisted
job1.join()
job2.join()

val storedStr = EventPipeline.readImpressions()
val storedDeserialized = Impression.Factory.fromJsonArray(JSONArray("[$storedStr]"))

assertThat(storedDeserialized).containsExactlyInAnyOrderElementsOf(impressions1 + impressions2)
}
}

@Test
fun clicks_are_batched() {
val clicks1 = listOf(
getClickPromoted(),
getClickOrganic(),
)
val clicks2 = listOf(
getClickPromoted(),
getClickOrganic(),
)

runBlocking {
EventPipeline.clear()
val job1 = EventPipeline.storeClick(
ClickEvent(clicks1),
shouldFlush = false
)
val job2 = EventPipeline.storeClick(
ClickEvent(clicks2),
shouldFlush = false
)

// Make sure they're persisted
job1.join()
job2.join()

val storedStr = EventPipeline.readClicks()
val storedDeserialized = Click.Factory.fromJsonArray(JSONArray("[$storedStr]"))

assertThat(storedDeserialized).containsExactlyInAnyOrderElementsOf(clicks1 + clicks2)
}
}

@Test
fun purchases_are_batched() {
val purchases1 = listOf(
getRandomPurchase(),
getRandomPurchase(),
)
val purchases2 = listOf(
getRandomPurchase(),
getRandomPurchase(),
)

runBlocking {
EventPipeline.clear()
val job1 = EventPipeline.storePurchase(
PurchaseEvent(purchases1),
shouldFlush = false
)
val job2 = EventPipeline.storePurchase(
PurchaseEvent(purchases2),
shouldFlush = false
)

// Make sure they're persisted
job1.join()
job2.join()

val storedStr = EventPipeline.readPurchases()
val storedDeserialized = Purchase.fromJsonArray(JSONArray("[$storedStr]"))

assertThat(storedDeserialized).containsExactlyInAnyOrderElementsOf(purchases1 + purchases2)
}
}

@Test
fun aggregate_joins_Events() {
val impressions = listOf(
getImpressionPromoted(),
getImpressionOrganic()
)

val clicks = listOf(
getClickPromoted(),
getClickOrganic(),
)

val purchases = listOf(
getRandomPurchase(),
getRandomPurchase(),
)

runBlocking {
EventPipeline.clear()
val aggregated = Event(
impressions = impressions,
clicks = clicks,
purchases = purchases
)
val job1 = EventPipeline.storeImpression(
ImpressionEvent(impressions),
shouldFlush = false
)
val job2 = EventPipeline.storeClick(
ClickEvent(clicks),
shouldFlush = false
)
val job3 = EventPipeline.storePurchase(
PurchaseEvent(purchases),
shouldFlush = false
)

// Make sure they're persisted
job1.join()
job2.join()
job3.join()

val storedEvent = EventPipeline.aggregateEvents()

assertThat(aggregated).isEqualTo(storedEvent)
}
}

@Test
fun events_are_uploaded() {
val impressions = listOf(
getImpressionPromoted(),
getImpressionOrganic()
)

val aggregated = Event(
impressions = impressions,
)

runBlocking {
EventPipeline.clear()

EventPipeline.storeImpression(ImpressionEvent(impressions))
.join()

// Wait for the upload work to be done
while (
workManager.getWorkInfos(WorkQuery.fromUniqueWorkNames(UPLOAD_SIGNAL))
.get().first().state != WorkInfo.State.SUCCEEDED
) {
yield()
}

}

assertThat(Logger.log).contains("uploading: ${aggregated.toJsonObject()}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.topsort.analytics

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.topsort.analytics.model.Click
import com.topsort.analytics.model.Impression
import com.topsort.analytics.model.Purchase
import org.assertj.core.api.Assertions.assertThat
import org.json.JSONObject
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
internal class JsonTestAndroid {

@Test
fun json_click_serialization() {
val clicks = listOf(
getClickPromoted(),
getClickOrganic()
)

for(click in clicks) {
val serialized = click.toJsonObject().toString()
val deserialized = Click.Factory.fromJsonObject(JSONObject(serialized))

assertThat(click).isNotSameAs(deserialized)
assertThat(click).isEqualTo(deserialized)
}
}

@Test
fun json_impression_serialization() {
val impressions = listOf(
getImpressionPromoted(),
getImpressionOrganic()
)

for(impression in impressions) {
val serialized = impression.toJsonObject().toString()
val deserialized = Impression.Factory.fromJsonObject(JSONObject(serialized))

assertThat(impression).isNotSameAs(deserialized)
assertThat(impression).isEqualTo(deserialized)
}
}

@Test
fun json_purchase_serialization() {
val purchase = getRandomPurchase()
val serialized = purchase.toJsonObject().toString()
val deserialized = Purchase.fromJsonObject(JSONObject(serialized))

assertThat(purchase).isNotSameAs(deserialized)
assertThat(purchase).isEqualTo(deserialized)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.topsort.analytics

import com.topsort.analytics.core.eventNow
import com.topsort.analytics.core.randomId
import com.topsort.analytics.model.Click
import com.topsort.analytics.model.Entity
import com.topsort.analytics.model.EntityType
import com.topsort.analytics.model.Impression
import com.topsort.analytics.model.Placement
import com.topsort.analytics.model.Purchase
import com.topsort.analytics.model.PurchasedItem

fun getClickPromoted() : Click {
return Click.Factory.buildPromoted(
placement = getTestPlacement(),
occurredAt = eventNow(),
opaqueUserId = randomId("oId_"),
id = randomId("mktId_"),
resolvedBidId = randomId("resolvedBid_"),
additionalAttribution = "{\"additional\":\"attribution click\"}",
)
}

fun getClickOrganic() : Click {
return Click.Factory.buildOrganic(
placement = getTestPlacement(),
entity = Entity(
type = EntityType.Product,
id = randomId("product_"),
),
occurredAt = eventNow(),
opaqueUserId = randomId("oId_"),
id = randomId("mktId_"),
additionalAttribution = "{\"additional\":\"attribution click\"}",
)
}

fun getImpressionPromoted() : Impression {
return Impression.Factory.buildPromoted (
placement = getTestPlacement(),
occurredAt = eventNow(),
opaqueUserId = randomId("oId_"),
id = randomId("mktId_"),
resolvedBidId = randomId("resolvedBid_"),
additionalAttribution = "{\"additional\":\"attribution impression\"}",
)
}

fun getImpressionOrganic() : Impression {
return Impression.Factory.buildOrganic (
placement = getTestPlacement(),
entity = Entity(
type = EntityType.Product,
id = randomId("product_"),
),
occurredAt = eventNow(),
opaqueUserId = randomId("oId_"),
id = randomId("mktId_"),
additionalAttribution = "{\"additional\":\"attribution impression\"}",
)
}

fun getRandomPurchase() : Purchase {
return Purchase(
opaqueUserId = randomId("oId_"),
occurredAt = eventNow(),
items = listOf(
PurchasedItem(
productId = randomId("p_"),
quantity = 1,
unitPrice = 100,
resolvedBidId = randomId("resolvedBid_"),
)
),
id = randomId("orderId_"),
)
}

private fun getTestPlacement() : Placement {
return Placement(
path = "test",
position = 2,
page = 1,
pageSize = 20,
productId = randomId(),
categoryIds = listOf("cat1", "cat2"),
searchQuery = "search query",
location = "gibraltar",
)
}
Loading