From 56c8c36fbe470e74deb760096580ed2027108f5d Mon Sep 17 00:00:00 2001 From: cgee1 Date: Fri, 20 Nov 2020 08:31:43 -0800 Subject: [PATCH 1/3] add getBrowseResults --- .../java/io/constructor/core/ConstructorIo.kt | 19 + .../java/io/constructor/data/DataManager.kt | 25 + .../data/model/browse/BrowseData.kt | 6 + .../data/model/browse/BrowseFacet.kt | 14 + .../data/model/browse/BrowseGroup.kt | 9 + .../data/model/browse/BrowseResponse.kt | 5 + .../data/model/browse/BrowseResult.kt | 7 + .../io/constructor/data/remote/ApiPaths.kt | 3 + .../constructor/data/remote/ConstructorApi.kt | 3 + .../core/ConstructorioBrowseTest.kt | 119 ++ .../src/test/resources/browse_response.json | 1237 +++++++++++++++++ .../test/resources/browse_response_empty.json | 39 + 12 files changed, 1486 insertions(+) create mode 100644 library/src/main/java/io/constructor/data/model/browse/BrowseData.kt create mode 100644 library/src/main/java/io/constructor/data/model/browse/BrowseFacet.kt create mode 100644 library/src/main/java/io/constructor/data/model/browse/BrowseGroup.kt create mode 100644 library/src/main/java/io/constructor/data/model/browse/BrowseResponse.kt create mode 100644 library/src/main/java/io/constructor/data/model/browse/BrowseResult.kt create mode 100644 library/src/test/java/io/constructor/core/ConstructorioBrowseTest.kt create mode 100644 library/src/test/resources/browse_response.json create mode 100644 library/src/test/resources/browse_response_empty.json diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 4ee74c39..f678e5fe 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -9,6 +9,7 @@ import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.Group import io.constructor.data.model.Suggestion import io.constructor.data.model.search.SearchResponse +import io.constructor.data.model.browse.BrowseResponse import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -267,4 +268,22 @@ object ConstructorIo { val params = mutableListOf(Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) return dataManager.trackPurchase(clientIds.toList(), revenueString, orderID, params.toTypedArray()) } + + /** + * Returns browse results including filters, categories, sort options, etc. + */ + fun getBrowseResults(filterName: String, filterValue: String, facets: List>>? = null, page: Int? = null, perPage: Int? = null, groupId: Int? = null, sortBy: String? = null, sortOrder: String? = null): Observable> { + val encodedParams: ArrayList> = arrayListOf() + groupId?.let { encodedParams.add(Constants.QueryConstants.FILTER_GROUP_ID.urlEncode() to it.toString()) } + page?.let { encodedParams.add(Constants.QueryConstants.PAGE.urlEncode() to page.toString().urlEncode()) } + perPage?.let { encodedParams.add(Constants.QueryConstants.PER_PAGE.urlEncode() to perPage.toString().urlEncode()) } + sortBy?.let { encodedParams.add(Constants.QueryConstants.SORT_BY.urlEncode() to it.urlEncode()) } + sortOrder?.let { encodedParams.add(Constants.QueryConstants.SORT_ORDER.urlEncode() to it.urlEncode()) } + facets?.forEach { facet -> + facet.second.forEach { + encodedParams.add(Constants.QueryConstants.FILTER_FACET.format(facet.first).urlEncode() to it.urlEncode()) + } + } + return dataManager.getBrowseResults(filterName, filterValue, encodedParams = encodedParams.toTypedArray()) + } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index a0077e4c..67471d14 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -3,6 +3,7 @@ package io.constructor.data import com.squareup.moshi.Moshi import io.constructor.data.model.Suggestion import io.constructor.data.model.search.SearchResponse +import io.constructor.data.model.browse.BrowseResponse import io.constructor.data.remote.ApiPaths import io.constructor.data.remote.ConstructorApi import io.reactivex.Completable @@ -84,4 +85,28 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi return constructorApi.trackPurchase(customerIds, revenue, orderID, params.toMap()) } + fun getBrowseResults(filterName: String, filterValue: String, encodedParams: Array> = arrayOf()): Observable> { + var dynamicUrl = "/${ApiPaths.URL_BROWSE.format(filterName, filterValue)}" + encodedParams.forEachIndexed { index, pair -> + dynamicUrl += "${if (index != 0) "&" else "?" }${pair.first}=${pair.second}" + } + return constructorApi.getBrowseResults(dynamicUrl).map { result -> + if (!result.isError) { + result.response()?.let { + if (it.isSuccessful){ + val adapter = moshi.adapter(BrowseResponse::class.java) + val response = it.body()?.string() + val result = response?.let { adapter.fromJson(it) } + result?.rawData = response + ConstructorData.of(result!!) + } else { + ConstructorData.networkError(it.errorBody()?.string()) + } + } ?: ConstructorData.error(result.error()) + } else { + ConstructorData.error(result.error()) + } + }.toObservable() + } + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/browse/BrowseData.kt b/library/src/main/java/io/constructor/data/model/browse/BrowseData.kt new file mode 100644 index 00000000..2bb1f7ee --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/browse/BrowseData.kt @@ -0,0 +1,6 @@ +package io.constructor.data.model.browse + +import com.squareup.moshi.Json +import io.constructor.data.model.search.SortOption + +data class BrowseData(val facets: List?, val groups: List?, @Json(name = "results") val browseResults: List?, @Json(name = "sort_options") val sortOptions: List? = null, @Json(name = "total_num_results") val resultCount: Int) diff --git a/library/src/main/java/io/constructor/data/model/browse/BrowseFacet.kt b/library/src/main/java/io/constructor/data/model/browse/BrowseFacet.kt new file mode 100644 index 00000000..5189929d --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/browse/BrowseFacet.kt @@ -0,0 +1,14 @@ +package io.constructor.data.model.browse + +import com.squareup.moshi.Json +import java.io.Serializable +import io.constructor.data.model.search.FacetOption + + +data class BrowseFacet(val name: String, + @Json(name = "display_name") val displayName: String?, + val status: Map?, + val type: String?, + val min: Double?, + val max: Double?, + val options: List?) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/browse/BrowseGroup.kt b/library/src/main/java/io/constructor/data/model/browse/BrowseGroup.kt new file mode 100644 index 00000000..e276cea4 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/browse/BrowseGroup.kt @@ -0,0 +1,9 @@ +package io.constructor.data.model.browse + +import com.squareup.moshi.Json + +data class BrowseGroup(@Json(name = "children") val children: List?, + @Json(name = "parents") val parents: List?, + val count: Int?, + @Json(name = "display_name") val displayName: String, + @Json(name = "group_id") val groupId: String) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/browse/BrowseResponse.kt b/library/src/main/java/io/constructor/data/model/browse/BrowseResponse.kt new file mode 100644 index 00000000..a2455b53 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/browse/BrowseResponse.kt @@ -0,0 +1,5 @@ +package io.constructor.data.model.browse + +import com.squareup.moshi.Json + +data class BrowseResponse(@Json(name = "response") val browseData: BrowseData, @Json(name = "result_id") val resultId: String, var rawData: String?) \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/model/browse/BrowseResult.kt b/library/src/main/java/io/constructor/data/model/browse/BrowseResult.kt new file mode 100644 index 00000000..f16028e4 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/browse/BrowseResult.kt @@ -0,0 +1,7 @@ +package io.constructor.data.model.browse + +import com.squareup.moshi.Json +import io.constructor.data.model.search.ResultData +import java.io.Serializable + +data class BrowseResult(@Json(name = "data") val result: ResultData, @Json(name = "matched_terms") val matchedTerms: List?, val value: String) : Serializable diff --git a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index a7c7b135..8cf7f9c5 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -10,4 +10,7 @@ object ApiPaths { const val URL_BEHAVIOR = "behavior" const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase" const val URL_SEARCH = "search/%s" + const val URL_BROWSE = "browse/%s/%s" + const val URL_BROWSE_RESULT_CLICK_EVENT = "v2/behavioral_action/browse_result_click" + const val URL_BROWSE_RESULT_LOAD = "v2/behavioral_action/browse_result_load" } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index 4e0203bf..59a435a9 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -58,4 +58,7 @@ interface ConstructorApi { @GET fun getSearchResults(@Url searchUrl: String): Single> + @GET + fun getBrowseResults(@Url browseUrl: String): Single> + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorioBrowseTest.kt b/library/src/test/java/io/constructor/core/ConstructorioBrowseTest.kt new file mode 100644 index 00000000..03318ce4 --- /dev/null +++ b/library/src/test/java/io/constructor/core/ConstructorioBrowseTest.kt @@ -0,0 +1,119 @@ +package io.constructor.core + +import android.content.Context +import io.constructor.data.local.PreferencesHelper +import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.test.createTestDataManager +import io.constructor.util.RxSchedulersOverrideRule +import io.constructor.util.TestDataLoader +import io.mockk.every +import io.mockk.mockk +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.TimeUnit + +class ConstructorIoBrowseTest { + + @Rule + @JvmField + val overrideSchedulersRule = RxSchedulersOverrideRule() + + private lateinit var mockServer: MockWebServer + private var constructorIo = ConstructorIo + private val ctx = mockk() + private val preferencesHelper = mockk() + private val configMemoryHolder = mockk() + + @Before + fun setup() { + mockServer = MockWebServer() + mockServer.start() + + every { ctx.applicationContext } returns ctx + + every { preferencesHelper.apiKey } returns "silver-key" + every { preferencesHelper.id } returns "guapo-the-guid" + every { preferencesHelper.serviceUrl } returns mockServer.hostName + every { preferencesHelper.port } returns mockServer.port + every { preferencesHelper.scheme } returns "http" + every { preferencesHelper.defaultItemSection } returns "Products" + every { preferencesHelper.getSessionId(any(), any()) } returns 92 + + every { configMemoryHolder.autocompleteResultCount } returns null + every { configMemoryHolder.userId } returns "player-two" + every { configMemoryHolder.testCellParams } returns emptyList() + + val config = ConstructorIoConfig("dummyKey") + val dataManager = createTestDataManager(preferencesHelper, configMemoryHolder, ctx) + + constructorIo.testInit(ctx, config, dataManager, preferencesHelper, configMemoryHolder) + } + + @Test + fun getBrowseResults() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("browse_response.json")) + mockServer.enqueue(mockResponse) + val observer = constructorIo.getBrowseResults("group_id", "Beverages").test() + observer.assertComplete().assertValue { + it.get()!!.browseData.browseResults!!.size == 24 + it.get()!!.browseData.browseResults!![0].value == "Crystal Geyser Natural Alpine Spring Water - 1 Gallon" + it.get()!!.browseData.browseResults!![0].result.id == "108200440" + it.get()!!.browseData.browseResults!![0].result.imageUrl == "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-108200440.png" + it.get()!!.browseData.browseResults!![0].result.metadata["price"] == 1.25 + it.get()!!.browseData.facets!!.size == 3 + it.get()!!.browseData.facets!![0].displayName == "Brand" + it.get()!!.browseData.facets!![0].type == "multiple" + it.get()!!.browseData.groups!!.size == 1 + it.get()!!.browseData.resultCount == 367 + } + val request = mockServer.takeRequest() + val path = "/browse/group_id/Beverages?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-2.1.1&_dt" + assert(request.path.startsWith(path)) + } + + @Test + fun getBrowseResultsWithServerError() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = constructorIo.getBrowseResults("group_id", "Beverages").test() + observer.assertComplete().assertValue { + it.networkError + } + val request = mockServer.takeRequest() + val path = "/browse/group_id/Beverages?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-2.1.1&_dt" + assert(request.path.startsWith(path)) + } + + @Test + fun getBrowseResultsWithTimeout() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("browse_response.json")) + mockResponse.throttleBody(128, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = constructorIo.getBrowseResults("group_id", "Beverages").test() + observer.assertComplete().assertValue { + it.isError + } + val request = mockServer.takeRequest() + val path = "/browse/group_id/Beverages?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-2.1.1&_dt" + assert(request.path.startsWith(path)) + } + + @Test + fun getBrowseResultsWithEmptyResponse() { + val mockResponse = MockResponse().setResponseCode(200).setBody(TestDataLoader.loadAsString("browse_response_empty.json")) + mockServer.enqueue(mockResponse) + val observer = constructorIo.getBrowseResults("group_id", "Beverages").test() + observer.assertComplete().assertValue { + it.get()!!.browseData.browseResults!!.isEmpty() + it.get()!!.browseData.facets!!.isEmpty() + it.get()!!.browseData.groups!!.isEmpty() + it.get()!!.browseData.resultCount == 0 + } + val request = mockServer.takeRequest() + val path = "/browse/group_id/Beverages?key=silver-key&i=guapo-the-guid&ui=player-two&s=92&c=cioand-2.1.1&_dt" + assert(request.path.startsWith(path)) + } +} \ No newline at end of file diff --git a/library/src/test/resources/browse_response.json b/library/src/test/resources/browse_response.json new file mode 100644 index 00000000..8653515c --- /dev/null +++ b/library/src/test/resources/browse_response.json @@ -0,0 +1,1237 @@ +{ + "request": { + "page": 1, + "num_results_per_page": 24, + "section": "Products", + "browse_filter_name": "group_id", + "browse_filter_value": "Beverages", + "term": "", + "fmt_options": { + "groups_start": "current", + "groups_max_depth": 1 + }, + "sort_by": "relevance", + "sort_order": "descending", + "features": { + "query_items": true, + "auto_generated_refined_query_rules": true, + "personalization": true, + "manual_searchandizing": true, + "filter_items": true + }, + "feature_variants": { + "query_items": null, + "auto_generated_refined_query_rules": null, + "personalization": null, + "manual_searchandizing": null, + "filter_items": null + }, + "searchandized_items": {} + }, + "response": { + "facets": [ + { + "display_name": "Brand", + "name": "Brand", + "type": "multiple", + "options": [ + { + "status": "", + "count": 12, + "display_name": "Crystal Geyser", + "value": "Crystal Geyser", + "data": { + + } + }, + { + "status": "", + "count": 36, + "display_name": "Coca-Cola", + "value": "Coca-Cola", + "data": { + + } + }, + { + "status": "", + "count": 28, + "display_name": "PepsiCo", + "value": "PepsiCo", + "data": { + + } + }, + { + "status": "", + "count": 12, + "display_name": "Coke", + "value": "Coke", + "data": { + + } + }, + { + "status": "", + "count": 22, + "display_name": "Arrowhead", + "value": "Arrowhead", + "data": { + + } + }, + { + "status": "", + "count": 35, + "display_name": "Pepsi", + "value": "Pepsi", + "data": { + + } + }, + { + "status": "", + "count": 44, + "display_name": "Coca Cola", + "value": "Coca Cola", + "data": { + + } + }, + { + "status": "", + "count": 37, + "display_name": "JM Smucker", + "value": "JM Smucker", + "data": { + + } + }, + { + "status": "", + "count": 17, + "display_name": "Sprite", + "value": "Sprite", + "data": { + + } + }, + { + "status": "", + "count": 31, + "display_name": "Monster Beverage", + "value": "Monster Beverage", + "data": { + + } + }, + { + "status": "", + "count": 23, + "display_name": "Dr Pepper", + "value": "Dr Pepper", + "data": { + + } + }, + { + "status": "", + "count": 7, + "display_name": "Hershey", + "value": "Hershey", + "data": { + + } + }, + { + "status": "", + "count": 10, + "display_name": "LaCroix", + "value": "LaCroix", + "data": { + + } + }, + { + "status": "", + "count": 33, + "display_name": "Ocean Spray", + "value": "Ocean Spray", + "data": { + + } + }, + { + "status": "", + "count": 87, + "display_name": "Starbucks", + "value": "Starbucks", + "data": { + + } + }, + { + "status": "", + "count": 18, + "display_name": "Capri Sun", + "value": "Capri Sun", + "data": { + + } + }, + { + "status": "", + "count": 21, + "display_name": "Canada Dry", + "value": "Canada Dry", + "data": { + + } + }, + { + "status": "", + "count": 35, + "display_name": "PEPSICO, INC.", + "value": "PEPSICO, INC.", + "data": { + + } + }, + { + "status": "", + "count": 7, + "display_name": "Welchs", + "value": "Welchs", + "data": { + + } + }, + { + "status": "", + "count": 6, + "display_name": "Coffeemate", + "value": "Coffeemate", + "data": { + + } + }, + { + "status": "", + "count": 72, + "display_name": "Gatorade", + "value": "Gatorade", + "data": { + + } + }, + { + "status": "", + "count": 24, + "display_name": "V8", + "value": "V8", + "data": { + + } + }, + { + "status": "", + "count": 34, + "display_name": "Peets Coffee", + "value": "Peets Coffee", + "data": { + + } + }, + { + "status": "", + "count": 7, + "display_name": "Unilever", + "value": "Unilever", + "data": { + + } + }, + { + "status": "", + "count": 22, + "display_name": "Nestle Waters", + "value": "Nestle Waters", + "data": { + + } + }, + { + "status": "", + "count": 17, + "display_name": "Keurig", + "value": "Keurig", + "data": { + + } + }, + { + "status": "", + "count": 11, + "display_name": "A&W", + "value": "A&W", + "data": { + + } + }, + { + "status": "", + "count": 46, + "display_name": "O Organics", + "value": "O Organics", + "data": { + + } + }, + { + "status": "", + "count": 38, + "display_name": "Signature Kitchens", + "value": "Signature Kitchens", + "data": { + + } + }, + { + "status": "", + "count": 24, + "display_name": "Motts", + "value": "Motts", + "data": { + + } + }, + { + "status": "", + "count": 30, + "display_name": "Dr Pepper/Seven Up", + "value": "Dr Pepper/Seven Up", + "data": { + + } + }, + { + "status": "", + "count": 23, + "display_name": "Naked Juice", + "value": "Naked Juice", + "data": { + + } + }, + { + "status": "", + "count": 5, + "display_name": "Tree Top", + "value": "Tree Top", + "data": { + + } + }, + { + "status": "", + "count": 22, + "display_name": "Snapple", + "value": "Snapple", + "data": { + + } + }, + { + "status": "", + "count": 26, + "display_name": "Minute Maid", + "value": "Minute Maid", + "data": { + + } + }, + { + "status": "", + "count": 5, + "display_name": "Glaceau", + "value": "Glaceau", + "data": { + + } + }, + { + "status": "", + "count": 4, + "display_name": "7 Up", + "value": "7 Up", + "data": { + + } + }, + { + "status": "", + "count": 80, + "display_name": "Signature Select", + "value": "Signature Select", + "data": { + + } + }, + { + "status": "", + "count": 41, + "display_name": "Kraft Foods", + "value": "Kraft Foods", + "data": { + + } + }, + { + "status": "", + "count": 6, + "display_name": "Dole", + "value": "Dole", + "data": { + + } + }, + { + "status": "", + "count": 17, + "display_name": "vitaminwater", + "value": "vitaminwater", + "data": { + + } + }, + { + "status": "", + "count": 15, + "display_name": "Floridas Natural", + "value": "Floridas Natural", + "data": { + + } + }, + { + "status": "", + "count": 43, + "display_name": "AriZona", + "value": "AriZona", + "data": { + + } + }, + { + "status": "", + "count": 20, + "display_name": "Fanta", + "value": "Fanta", + "data": { + + } + }, + { + "status": "", + "count": 4, + "display_name": "Safeway", + "value": "Safeway", + "data": { + + } + }, + { + "status": "", + "count": 24, + "display_name": "F Gavina & Sons", + "value": "F Gavina & Sons", + "data": { + + } + }, + { + "status": "", + "count": 19, + "display_name": "Powerade", + "value": "Powerade", + "data": { + + } + }, + { + "status": "", + "count": 5, + "display_name": "S Martinelli", + "value": "S Martinelli", + "data": { + + } + }, + { + "status": "", + "count": 11, + "display_name": "Nestle", + "value": "Nestle", + "data": { + + } + }, + { + "status": "", + "count": 6, + "display_name": "Signature Farms", + "value": "Signature Farms", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "R.C. Bigelow, Inc.", + "value": "R.C. Bigelow, Inc.", + "data": { + + } + }, + { + "status": "", + "count": 4, + "display_name": "Safeway Inc.", + "value": "Safeway Inc.", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "Nesquik", + "value": "Nesquik", + "data": { + + } + }, + { + "status": "", + "count": 16, + "display_name": "Bai", + "value": "Bai", + "data": { + + } + }, + { + "status": "", + "count": 6, + "display_name": "Diet Coke", + "value": "Diet Coke", + "data": { + + } + }, + { + "status": "", + "count": 11, + "display_name": "Sunkist", + "value": "Sunkist", + "data": { + + } + }, + { + "status": "", + "count": 27, + "display_name": "Red Bull", + "value": "Red Bull", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "Yuban Coffee", + "value": "Yuban Coffee", + "data": { + + } + }, + { + "status": "", + "count": 13, + "display_name": "Kevita", + "value": "Kevita", + "data": { + + } + }, + { + "status": "", + "count": 5, + "display_name": "Natural Waters", + "value": "Natural Waters", + "data": { + + } + }, + { + "status": "", + "count": 22, + "display_name": "Bigelow Tea", + "value": "Bigelow Tea", + "data": { + + } + }, + { + "status": "", + "count": 8, + "display_name": "Schweppes", + "value": "Schweppes", + "data": { + + } + }, + { + "status": "", + "count": 28, + "display_name": "Kool-Aid", + "value": "Kool-Aid", + "data": { + + } + }, + { + "status": "", + "count": 5, + "display_name": "Pom Wonderful", + "value": "Pom Wonderful", + "data": { + + } + }, + { + "status": "", + "count": 17, + "display_name": "Rockstar", + "value": "Rockstar", + "data": { + + } + }, + { + "status": "", + "count": 8, + "display_name": "Cascade", + "value": "Cascade", + "data": { + + } + }, + { + "status": "", + "count": 6, + "display_name": "Voss", + "value": "Voss", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "Campbell Soup", + "value": "Campbell Soup", + "data": { + + } + }, + { + "status": "", + "count": 3, + "display_name": "Barqs", + "value": "Barqs", + "data": { + + } + }, + { + "status": "", + "count": 6, + "display_name": "ConAgra Foods", + "value": "ConAgra Foods", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "The Gatorade Co", + "value": "The Gatorade Co", + "data": { + + } + }, + { + "status": "", + "count": 26, + "display_name": "Twinings", + "value": "Twinings", + "data": { + + } + }, + { + "status": "", + "count": 11, + "display_name": "Bolthouse Farms", + "value": "Bolthouse Farms", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "Ito En", + "value": "Ito En", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "Farmer Brothers", + "value": "Farmer Brothers", + "data": { + + } + }, + { + "status": "", + "count": 2, + "display_name": "CJR Bottling", + "value": "CJR Bottling", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "VitaNourish", + "value": "VitaNourish", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "VitaCup", + "value": "VitaCup", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "TC Heartland", + "value": "TC Heartland", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "Southern Tea", + "value": "Southern Tea", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "Bread & Chocolate", + "value": "Bread & Chocolate", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "Brad Barry", + "value": "Brad Barry", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "Aspire Beverage", + "value": "Aspire Beverage", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "American Beverage", + "value": "American Beverage", + "data": { + + } + }, + { + "status": "", + "count": 1, + "display_name": "11 Productions", + "value": "11 Productions", + "data": { + + } + } + ], + "data": { + + } + }, + { + "display_name": "Nutrition", + "name": "Nutrition", + "type": "multiple", + "options": [ + { + "status": "", + "count": 1193, + "display_name": "Kosher", + "value": "Kosher", + "data": null + }, + { + "status": "", + "count": 360, + "display_name": "Gluten Free", + "value": "Gluten Free", + "data": { + + } + }, + { + "status": "", + "count": 163, + "display_name": "Sugar Free", + "value": "Sugar Free", + "data": { + + } + }, + { + "status": "", + "count": 21, + "display_name": "Fat Free", + "value": "Fat Free", + "data": { + + } + }, + { + "status": "", + "count": 19, + "display_name": "Low Fat", + "value": "Low Fat", + "data": { + + } + }, + { + "status": "", + "count": 135, + "display_name": "Organic", + "value": "Organic", + "data": { + + } + } + ], + "data": { + + } + }, + { + "min": 0.77, + "max": 28.09, + "display_name": "Price", + "name": "Price", + "data": { + + }, + "type": "range", + "status": { + + } + } + ], + "groups": [ + { + "group_id": "Beverages", + "display_name": "Beverages", + "count": 2126, + "data": null, + "children": [ + + ], + "parents": [ + { + "display_name": "All", + "group_id": "all" + } + ] + } + ], + "results": [ + { + "matched_terms": [ + + ], + "data": { + "id": "108200440", + "url": "/", + "price": 1.25, + "facets": [ + { + "name": "Brand", + "values": [ + "Crystal Geyser" + ] + }, + { + "name": "Inventory", + "values": [ + 517 + ] + }, + { + "name": "Margin", + "values": [ + 0.07 + ] + }, + { + "name": "Nutrition", + "values": [ + "Kosher" + ] + }, + { + "name": "Price", + "values": [ + 1.25 + ] + } + ], + "keywords": [ + "water" + ], + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-108200440.png", + "description": "Bottled at the source. CrystalGeyserPlease.com. Bottled at the source, of course. Look closely at our source map on each bottle of Crystal Geyser Alpine Spring Water and locate our origins. No one else shows you a source map. We do because it's easy to tell the truth. Crystal Geyser bottles 100% of its water at the spring source; we're the only national spring water that does. Please recycle. Find us on Facebook. American Forests americanforests.org. Making a difference: CG Roxane is a proud sponsor of American Forests' tree-planting for environmental restoration. Crystal Geyser Alpine Spring Water: Always bottled at the spring to ensure quality, taste and freshness. There is a difference. For more information and to obtain additional consumer information relating to water quality, including a bottled water report, contact CG Roxane LLC at 1-800-4-GEYSER. Comments? 1-800-4-GEYSER.", + "weighted_keywords": { + "water": 2 + }, + "groups": [ + { + "group_id": "Beverages", + "display_name": "Beverages", + "path": "/all", + "path_list": [ + { + "id": "all", + "display_name": "All" + } + ] + } + ] + }, + "value": "Crystal Geyser Natural Alpine Spring Water - 1 Gallon", + "is_slotted": false + }, + { + "matched_terms": [ + + ], + "data": { + "id": "108010222", + "url": "/", + "price": 8.43, + "facets": [ + { + "name": "Brand", + "values": [ + "Coca-Cola" + ] + }, + { + "name": "Inventory", + "values": [ + 590 + ] + }, + { + "name": "Margin", + "values": [ + 0.11 + ] + }, + { + "name": "Price", + "values": [ + 8.43 + ] + } + ], + "keywords": [ + "cola" + ], + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-108010222.png", + "description": "Original taste. Per 1 Can Serving: 140 calories; 0 g sat fat (0% DV); 45 mg sodium (2% DV); 39 g sugars. Please recycle cans & cartons. Caffeine Content: 34 mg/12 fl oz. Consumer information call 1-800-438-2653. coke.com. SmartLabel. Sip & Scan. No app required. Open coke.com on phone. Scan icon. Enjoy more.", + "groups": [ + { + "group_id": "Beverages", + "display_name": "Beverages", + "path": "/all", + "path_list": [ + { + "id": "all", + "display_name": "All" + } + ] + } + ] + }, + "value": "Coca-Cola Soda - 12-12 Fl. Oz.", + "is_slotted": false + }, + { + "matched_terms": [ + + ], + "data": { + "id": "960035196", + "url": "/", + "price": 7.99, + "facets": [ + { + "name": "Brand", + "values": [ + "Coca-Cola" + ] + }, + { + "name": "Inventory", + "values": [ + 727 + ] + }, + { + "name": "Margin", + "values": [ + 0.05 + ] + }, + { + "name": "Price", + "values": [ + 7.99 + ] + } + ], + "keywords": [ + "cola" + ], + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960035196.png", + "description": "Per 1 Can Serving: 140 calories; 0 g sat fat (0% DV); 45 mg sodium (2% DV); 39 g sugars. MyCokeRewards.com. Please recycle cans & cartons. Find the code inside. Enter code online at MyCokeRewards.com. See rules online or call 1-866-674-2653, open to legal residents of the 50 US (& D.C.) age 13 and older. Low sodium. Caffeine Content: 34 mg/12 fl oz. Consumer information call 1-800-438-2653 or Coke.com. Coke original formula.", + "groups": [ + { + "group_id": "Beverages", + "display_name": "Beverages", + "path": "/all", + "path_list": [ + { + "id": "all", + "display_name": "All" + } + ] + } + ] + }, + "value": "Coca-Cola Classic Soda - 20-12 Fl. Oz.", + "is_slotted": false + }, + { + "matched_terms": [ + + ], + "data": { + "id": "960160365", + "url": "/", + "price": 1.11, + "facets": [ + { + "name": "Brand", + "values": [ + "Crystal Geyser" + ] + }, + { + "name": "Inventory", + "values": [ + 659 + ] + }, + { + "name": "Margin", + "values": [ + 0.12 + ] + }, + { + "name": "Price", + "values": [ + 1.11 + ] + } + ], + "keywords": [ + "water" + ], + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960160365.png", + "description": "Bottled at the source. 100% natural spring water. There is a difference! All purified waters are man made. Crystal Geyser Alpine Spring Water is natural. Things are that simple. Now in new eco-friendly packaging. Bottled at the Spring source Olancha, California; Weed, California; Benton, Tennessee; Moultonborough, New Hampshire, Salem, South Carolina; Normal, Arkansas; Johnstown, New York; See bottle label for source and license information. www.crystalgeyserasw.com.", + "groups": [ + { + "group_id": "Beverages", + "display_name": "Beverages", + "path": "/all", + "path_list": [ + { + "id": "all", + "display_name": "All" + } + ] + } + ] + }, + "value": "Crystal Geyser Spring Water Natural Alpine - 24-16.9 Fl. Oz.", + "is_slotted": false + }, + { + "matched_terms": [ + + ], + "data": { + "id": "960137162", + "url": "/", + "price": 1.25, + "facets": [ + { + "name": "Brand", + "values": [ + "Crystal Geyser" + ] + }, + { + "name": "Inventory", + "values": [ + 413 + ] + }, + { + "name": "Margin", + "values": [ + 0.1 + ] + }, + { + "name": "Nutrition", + "values": [ + "Kosher" + ] + }, + { + "name": "Price", + "values": [ + 1.25 + ] + } + ], + "keywords": [ + "water", + "sparkling" + ], + "image_url": "https://d17bbgoo3npfov.cloudfront.net/images/farmstand-960137162.png", + "description": "No calories. Very low sodium. Contains no juice. The ultimate refresher.", + "groups": [ + { + "group_id": "Beverages", + "display_name": "Beverages", + "path": "/all", + "path_list": [ + { + "id": "all", + "display_name": "All" + } + ] + } + ] + }, + "value": "Crystal Geyser Mineral Water Sparkling Natural Lime Flavor Bonus - 42.3 Fl. Oz.", + "is_slotted": false + } + ], + "sort_options": [ + + ], + "total_num_results": 367, + "features": [ + { + "feature_name": "auto_generated_refined_query_rules", + "display_name": "Affinity Engine", + "enabled": true, + "variant": null + }, + { + "feature_name": "filter_items", + "display_name": "Filter-item boosts", + "enabled": true, + "variant": null + }, + { + "feature_name": "manual_searchandizing", + "display_name": "Searchandizing", + "enabled": true, + "variant": null + }, + { + "feature_name": "personalization", + "display_name": "Personalization", + "enabled": true, + "variant": null + }, + { + "feature_name": "query_items", + "display_name": "Learn To Rank", + "enabled": true, + "variant": null + } + ] + }, + "result_id": "4cca9bb4-b5c4-4e15-af7a-5d68ee557d93" +} \ No newline at end of file diff --git a/library/src/test/resources/browse_response_empty.json b/library/src/test/resources/browse_response_empty.json new file mode 100644 index 00000000..c6c53628 --- /dev/null +++ b/library/src/test/resources/browse_response_empty.json @@ -0,0 +1,39 @@ +{ + "request": { + "page": 1, + "num_results_per_page": 24, + "section": "Products", + "browse_filter_name": "group_id", + "browse_filter_value": "Beverages", + "term": "", + "fmt_options": { + "groups_start": "current", + "groups_max_depth": 1 + }, + "sort_by": "relevance", + "sort_order": "descending", + "features": { + "query_items": true, + "auto_generated_refined_query_rules": true, + "personalization": true, + "manual_searchandizing": true, + "filter_items": true + }, + "feature_variants": { + "query_items": null, + "auto_generated_refined_query_rules": null, + "personalization": null, + "manual_searchandizing": null, + "filter_items": null + }, + "searchandized_items": {} + }, + "response": { + "facets": [], + "groups": [], + "results": [], + "sort_options": [], + "total_num_results": 0 + }, + "result_id": "4cca9bb4-b5c4-4e15-af7a-5d68ee557d93" +} \ No newline at end of file From 19590659a0b7d42b4a5fef2136d43fe3ca22d2a2 Mon Sep 17 00:00:00 2001 From: cgee1 Date: Fri, 20 Nov 2020 11:24:25 -0800 Subject: [PATCH 2/3] Add trackBrowseResultsClick and trackBrowseResultsLoaded --- .../java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 37 +++++++++ .../java/io/constructor/data/DataManager.kt | 8 ++ .../io/constructor/data/remote/ApiPaths.kt | 2 +- .../constructor/data/remote/ConstructorApi.kt | 14 ++++ .../core/ConstructorIoTrackingTest.kt | 79 +++++++++++++++++++ 6 files changed, 140 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index d2229fe9..eb25376f 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -43,6 +43,7 @@ class Constants { const val SEARCH_SUGGESTIONS = "Search Suggestions" const val PRODUCTS = "Products" const val EVENT_SEARCH_RESULTS = "search-results" + const val EVENT_BROWSE_RESULTS = "browse-results" const val EVENT_INPUT_FOCUS = "focus" } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index f678e5fe..7fceb078 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -286,4 +286,41 @@ object ConstructorIo { } return dataManager.getBrowseResults(filterName, filterValue, encodedParams = encodedParams.toTypedArray()) } + + /** + * Tracks browse results loaded (a.k.a. browse results viewed) events + */ + fun trackBrowseResultsLoaded(filterName: String, filterValue: String, resultCount: Int) { + var completable = trackBrowseResultsLoadedInternal(filterName, filterValue, resultCount) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Browse Results Loaded error: ${t.message}") + })) + } + internal fun trackBrowseResultsLoadedInternal(filterName: String, filterValue: String, resultCount: Int): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + return dataManager.trackBrowseResultsLoaded(filterName, filterValue, resultCount, arrayOf( + Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_BROWSE_RESULTS + )) + } + + /** + * Tracks browse result click events + */ + fun trackBrowseResultClick(filterName: String, filterValue: String, itemName: String, customerId: String, sectionName: String? = null, resultID: String? = null) { + var completable = trackBrowseResultClickInternal(filterName, filterValue, itemName, customerId, sectionName, resultID) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Browse Result Click error: ${t.message}") + })) + } + internal fun trackBrowseResultClickInternal(filterName: String, filterValue: String, itemName: String, customerId: String, sectionName: String? = null, resultID: String? = null): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val encodedParams: ArrayList> = arrayListOf() + resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) } + val sName = sectionName ?: preferenceHelper.defaultItemSection + return dataManager.trackBrowseResultClick(filterName, filterValue, itemName, customerId, arrayOf( + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName + ), encodedParams.toTypedArray()) + + } + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 67471d14..b7b578c0 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -109,4 +109,12 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi }.toObservable() } + fun trackBrowseResultClick(filterName: String, filterValue: String, itemName: String, customerId: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackBrowseResultClick(filterName, filterValue, itemName, customerId, params.toMap(), encodedParams.toMap()) + } + + fun trackBrowseResultsLoaded(filterName: String, filterValue: String, resultCount: Int, params: Array>): Completable { + return constructorApi.trackBrowseResultsLoaded(filterName, filterValue, resultCount, params.toMap()) + } + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index 8cf7f9c5..9f1cd0ec 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -12,5 +12,5 @@ object ApiPaths { const val URL_SEARCH = "search/%s" const val URL_BROWSE = "browse/%s/%s" const val URL_BROWSE_RESULT_CLICK_EVENT = "v2/behavioral_action/browse_result_click" - const val URL_BROWSE_RESULT_LOAD = "v2/behavioral_action/browse_result_load" + const val URL_BROWSE_RESULT_LOAD_EVENT = "v2/behavioral_action/browse_result_load" } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index 59a435a9..b3951138 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -61,4 +61,18 @@ interface ConstructorApi { @GET fun getBrowseResults(@Url browseUrl: String): Single> + @GET(ApiPaths.URL_BROWSE_RESULT_CLICK_EVENT) + fun trackBrowseResultClick(@Query("filter_name") filterName: String, + @Query("filter_value") filterValue: String, + @Query("name") itemName: String, + @Query("customer_id") customerId: String, + @QueryMap params: Map, + @QueryMap(encoded = true) encodedData: Map): Completable + + @GET(ApiPaths.URL_BROWSE_RESULT_LOAD_EVENT) + fun trackBrowseResultsLoaded(@Query("filter_name") filterName: String, + @Query("filter_value") filterValue: String, + @Query("num_results") resultCount: Int, + @QueryMap params: Map): Completable + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt index d153e8d4..acaa4c00 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt @@ -356,4 +356,83 @@ class ConstructorIoTest { val path = "/autocomplete/TERM_UNKNOWN/purchase?customer_ids=TIT-REP-1997&customer_ids=QE2-REP-1969&revenue=12.99&order_id=ORD-1312343&autocomplete_section=Recommendations&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; assert(request.path.startsWith(path)) } + + @Test + fun trackBrowseResultLoaded() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultsLoadedInternal("group_id", "Movies", 10).test() + observer.assertComplete() + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_load?filter_name=group_id&filter_value=Movies&num_results=10&action=browse-results&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } + + @Test + fun trackBrowseResultLoaded500() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultsLoadedInternal("group_id", "Movies", 10).test() + observer.assertError { true } + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_load?filter_name=group_id&filter_value=Movies&num_results=10&action=browse-results&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } + + @Test + fun trackBrowseResultLoadedTimeout() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultsLoadedInternal("group_id", "Movies", 10).test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_load?filter_name=group_id&filter_value=Movies&num_results=10&action=browse-results&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } + + @Test + fun trackBrowseResultClick() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997").test() + observer.assertComplete() + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } + + @Test + fun trackBrowseResultClickWithSectionAndResultID() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997", "Products", "3467632").test() + observer.assertComplete() + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&result_id=3467632&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } + + @Test + fun trackBrowseResultClick500() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997").test() + observer.assertError { true } + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } + + @Test + fun trackBrowseResultClickTimeout() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997").test() + observer.assertError(SocketTimeoutException::class.java) + val request = mockServer.takeRequest() + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + assert(request.path.startsWith(path)) + } } From bea8ebb5e06a901826b268675bf81631e343bf66 Mon Sep 17 00:00:00 2001 From: cgee1 Date: Fri, 20 Nov 2020 14:31:18 -0800 Subject: [PATCH 3/3] Update readme --- README.md | 27 ++++++++++++++++++- .../java/io/constructor/core/ConstructorIo.kt | 8 +++--- .../java/io/constructor/data/DataManager.kt | 4 +-- .../constructor/data/remote/ConstructorApi.kt | 2 +- .../core/ConstructorIoTrackingTest.kt | 17 ++++++------ 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ed21cb28..37b772c6 100755 --- a/README.md +++ b/README.md @@ -65,7 +65,22 @@ ConstructorIo.getSearchResults(query, selectedFacets?.map { it.key to it.value } ## 6. Request Browse Events ```kotlin -// Coming end of October +var page = 1 +var perPage = 10 +var filterName = "group_id" +var filterValue = "Beverages" +var selectedFacets: HashMap>? = null +var selectedSortOption: SortOption? = null + +ConstructorIo.getBrowseResults(filterName, filterValue, selectedFacets?.map { it.key to it.value }, page = page, perPage = limit, sortBy = selectedSortOption?.sortBy, sortOrder = selectedSortOption?.sortOrder) +.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) +.subscribe { + it.onValue { + it.browseData?.let { + view.renderData(it) + } + } +} ``` ## 7. Instrument Behavioral Events @@ -106,3 +121,13 @@ ConstructorIo.trackConversion("Fashionable Toothpicks", "1234567-AB", 12.99, "to // Track when products are purchased (customerIds, revenue, orderId) ConstructorIo.trackPurchase(arrayOf("1234567-AB", "1234567-AB"), 25.98, "ORD-1312343") ``` + +### Browse Events + +```kotlin +// Track when browse results are loaded into view (filterName, filterValue, resultCount) +ConstructorIo.trackBrowseResultsLoaded("Category", "Snacks", 674) + +// Track when a browse result is clicked (filterName, filterValue, customerId, resultPositionOnPage) +ConstructorIo.trackBrowseResultClick("Category", "Snacks", "7654321-BA", "4") +``` diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 7fceb078..952c7c5a 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -306,18 +306,18 @@ object ConstructorIo { /** * Tracks browse result click events */ - fun trackBrowseResultClick(filterName: String, filterValue: String, itemName: String, customerId: String, sectionName: String? = null, resultID: String? = null) { - var completable = trackBrowseResultClickInternal(filterName, filterValue, itemName, customerId, sectionName, resultID) + fun trackBrowseResultClick(filterName: String, filterValue: String, customerId: String, resultPositionOnPage: Int, sectionName: String? = null, resultID: String? = null) { + var completable = trackBrowseResultClickInternal(filterName, filterValue, customerId, resultPositionOnPage, sectionName, resultID) disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { t -> e("Browse Result Click error: ${t.message}") })) } - internal fun trackBrowseResultClickInternal(filterName: String, filterValue: String, itemName: String, customerId: String, sectionName: String? = null, resultID: String? = null): Completable { + internal fun trackBrowseResultClickInternal(filterName: String, filterValue: String, customerId: String, resultPositionOnPage: Int, sectionName: String? = null, resultID: String? = null): Completable { preferenceHelper.getSessionId(sessionIncrementHandler) val encodedParams: ArrayList> = arrayListOf() resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) } val sName = sectionName ?: preferenceHelper.defaultItemSection - return dataManager.trackBrowseResultClick(filterName, filterValue, itemName, customerId, arrayOf( + return dataManager.trackBrowseResultClick(filterName, filterValue, customerId, resultPositionOnPage, arrayOf( Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName ), encodedParams.toTypedArray()) diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index b7b578c0..25a4d79f 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -109,8 +109,8 @@ constructor(private val constructorApi: ConstructorApi, private val moshi: Moshi }.toObservable() } - fun trackBrowseResultClick(filterName: String, filterValue: String, itemName: String, customerId: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { - return constructorApi.trackBrowseResultClick(filterName, filterValue, itemName, customerId, params.toMap(), encodedParams.toMap()) + fun trackBrowseResultClick(filterName: String, filterValue: String, customerId: String, resultPositionOnPage: Int, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackBrowseResultClick(filterName, filterValue, customerId, resultPositionOnPage, params.toMap(), encodedParams.toMap()) } fun trackBrowseResultsLoaded(filterName: String, filterValue: String, resultCount: Int, params: Array>): Completable { diff --git a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt index b3951138..79ab699a 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -64,8 +64,8 @@ interface ConstructorApi { @GET(ApiPaths.URL_BROWSE_RESULT_CLICK_EVENT) fun trackBrowseResultClick(@Query("filter_name") filterName: String, @Query("filter_value") filterValue: String, - @Query("name") itemName: String, @Query("customer_id") customerId: String, + @Query("result_position_on_page") resultPositionOnPage: Int, @QueryMap params: Map, @QueryMap(encoded = true) encodedData: Map): Completable diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt index acaa4c00..4eaf61e4 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt @@ -395,10 +395,10 @@ class ConstructorIoTest { fun trackBrowseResultClick() { val mockResponse = MockResponse().setResponseCode(204) mockServer.enqueue(mockResponse) - val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997").test() + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","TIT-REP-1997", 4).test() observer.assertComplete() val request = mockServer.takeRequest() - val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&customer_id=TIT-REP-1997&result_position_on_page=4&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; assert(request.path.startsWith(path)) } @@ -406,10 +406,10 @@ class ConstructorIoTest { fun trackBrowseResultClickWithSectionAndResultID() { val mockResponse = MockResponse().setResponseCode(204) mockServer.enqueue(mockResponse) - val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997", "Products", "3467632").test() + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","TIT-REP-1997", 4, "Products", "3467632").test() observer.assertComplete() val request = mockServer.takeRequest() - val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&result_id=3467632&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&customer_id=TIT-REP-1997&result_position_on_page=4&autocomplete_section=Products&result_id=3467632&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; assert(request.path.startsWith(path)) } @@ -417,10 +417,10 @@ class ConstructorIoTest { fun trackBrowseResultClick500() { val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") mockServer.enqueue(mockResponse) - val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997").test() + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","TIT-REP-1997", 4).test() observer.assertError { true } val request = mockServer.takeRequest() - val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&customer_id=TIT-REP-1997&result_position_on_page=4&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; assert(request.path.startsWith(path)) } @@ -429,10 +429,11 @@ class ConstructorIoTest { val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") mockResponse.throttleBody(0, 5, TimeUnit.SECONDS) mockServer.enqueue(mockResponse) - val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","titanic replica", "TIT-REP-1997").test() + val observer = ConstructorIo.trackBrowseResultClickInternal("group_id", "Movies","TIT-REP-1997", 4).test() observer.assertError(SocketTimeoutException::class.java) val request = mockServer.takeRequest() - val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&name=titanic%20replica&customer_id=TIT-REP-1997&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; + val path = "/v2/behavioral_action/browse_result_click?filter_name=group_id&filter_value=Movies&customer_id=TIT-REP-1997&result_position_on_page=4&autocomplete_section=Products&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.1.1&_dt="; assert(request.path.startsWith(path)) } + }