-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(sdk): swap out sharedpreferences (#11)
Introduces the EventPipeline, in which events are collected and persisted asynchronously (with relation the main thread), and batched for upload. Closes: https://topsort.atlassian.net/browse/API-797 https://topsort.atlassian.net/browse/API-800 PR: #11
- Loading branch information
Showing
14 changed files
with
737 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
229 changes: 229 additions & 0 deletions
229
TopsortAnalytics/src/androidTest/java/com/topsort/analytics/EventPipelineTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}") | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
TopsortAnalytics/src/androidTest/java/com/topsort/analytics/JsonTestAndroid.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
TopsortAnalytics/src/androidTest/java/com/topsort/analytics/TestObjectsAndroid.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
) | ||
} |
Oops, something went wrong.