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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, MutableList<String>>? = 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
Expand Down Expand Up @@ -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")
```
1 change: 1 addition & 0 deletions library/src/main/java/io/constructor/core/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
56 changes: 56 additions & 0 deletions library/src/main/java/io/constructor/core/ConstructorIo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -267,4 +268,59 @@ 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<Pair<String, List<String>>>? = null, page: Int? = null, perPage: Int? = null, groupId: Int? = null, sortBy: String? = null, sortOrder: String? = null): Observable<ConstructorData<BrowseResponse>> {
val encodedParams: ArrayList<Pair<String, String>> = 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())
}

/**
* 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, 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, customerId: String, resultPositionOnPage: Int, sectionName: String? = null, resultID: String? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val encodedParams: ArrayList<Pair<String, String>> = arrayListOf()
resultID?.let { encodedParams.add(Constants.QueryConstants.RESULT_ID.urlEncode() to it.urlEncode()) }
val sName = sectionName ?: preferenceHelper.defaultItemSection
return dataManager.trackBrowseResultClick(filterName, filterValue, customerId, resultPositionOnPage, arrayOf(
Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName
), encodedParams.toTypedArray())

}

}
33 changes: 33 additions & 0 deletions library/src/main/java/io/constructor/data/DataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,4 +85,36 @@ 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<Pair<String, String>> = arrayOf()): Observable<ConstructorData<BrowseResponse>> {
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()
}

fun trackBrowseResultClick(filterName: String, filterValue: String, customerId: String, resultPositionOnPage: Int, params: Array<Pair<String, String>> = arrayOf(), encodedParams: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackBrowseResultClick(filterName, filterValue, customerId, resultPositionOnPage, params.toMap(), encodedParams.toMap())
}

fun trackBrowseResultsLoaded(filterName: String, filterValue: String, resultCount: Int, params: Array<Pair<String, String>>): Completable {
return constructorApi.trackBrowseResultsLoaded(filterName, filterValue, resultCount, params.toMap())
}

}
Original file line number Diff line number Diff line change
@@ -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<BrowseFacet>?, val groups: List<BrowseGroup>?, @Json(name = "results") val browseResults: List<BrowseResult>?, @Json(name = "sort_options") val sortOptions: List<SortOption>? = null, @Json(name = "total_num_results") val resultCount: Int)
Original file line number Diff line number Diff line change
@@ -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<String, Any>?,
val type: String?,
val min: Double?,
val max: Double?,
val options: List<FacetOption>?) : Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.constructor.data.model.browse

import com.squareup.moshi.Json

data class BrowseGroup(@Json(name = "children") val children: List<BrowseGroup>?,
@Json(name = "parents") val parents: List<BrowseGroup>?,
val count: Int?,
@Json(name = "display_name") val displayName: String,
@Json(name = "group_id") val groupId: String)
Original file line number Diff line number Diff line change
@@ -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?)
Original file line number Diff line number Diff line change
@@ -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<String>?, val value: String) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -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_EVENT = "v2/behavioral_action/browse_result_load"
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,21 @@ interface ConstructorApi {
@GET
fun getSearchResults(@Url searchUrl: String): Single<Result<ResponseBody>>

@GET
fun getBrowseResults(@Url browseUrl: String): Single<Result<ResponseBody>>

@GET(ApiPaths.URL_BROWSE_RESULT_CLICK_EVENT)
fun trackBrowseResultClick(@Query("filter_name") filterName: String,
@Query("filter_value") filterValue: String,
@Query("customer_id") customerId: String,
@Query("result_position_on_page") resultPositionOnPage: Int,
@QueryMap params: Map<String, String>,
@QueryMap(encoded = true) encodedData: Map<String, String>): 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<String, String>): Completable

}
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,84 @@ 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","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&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))
}

@Test
fun trackBrowseResultClickWithSectionAndResultID() {
val mockResponse = MockResponse().setResponseCode(204)
mockServer.enqueue(mockResponse)
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&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))
}

@Test
fun trackBrowseResultClick500() {
val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error")
mockServer.enqueue(mockResponse)
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&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))
}

@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","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&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))
}

}
Loading