diff --git a/README.md b/README.md index 9d2e530c..739a6647 100755 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ An Android Client for [Constructor.io](http://constructor.io/). [Constructor.io Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.1.0) to add the client to your project. ## 2. Retrieve an API key + You can find this in your [Constructor.io dashboard](https://constructor.io/dashboard). Contact sales if you'd like to sign up, or support if you believe your company already has an account. ## 3. Implement the Autocomplete UI @@ -136,18 +137,42 @@ The Android Client sends behavioral events to [Constructor.io](http://constructo Three types of these events exist: 1. **General Events** are sent as needed when an instance of the Client is created or initialized -1. **Autocomplete Events** measure user interaction with autocomplete results and the `CIOAutocompleteViewController` sends them automatically. +1. **Autocomplete Events** measure user interaction with autocomplete results and extending from `BaseSuggestionFragment` sends them automatically. 1. **Search Events** measure user interaction with search results and the consuming app has to explicitly instrument them itself +### Autocomplete Events + +If you decide to extend from the `BaseSuggestionFragment`, these events are sent automatically. + ```kotlin import io.constructor.core.ConstructorIo -// Track search results loaded (term, resultCount) -ConstructorIo.trackSearchResultLoaded("a search term", 123) +// Track when the user focuses into the search bar (searchTerm) +ConstructorIo.trackInputFocus("") -// Track search result click (term, itemId, position) -ConstructorIo.trackSearchResultClickThrough("a search term", "an item id", "1") +// Track when the user selects an autocomplete suggestion (searchTerm, originalQuery, sectionName) +ConstructorIo.trackAutocompleteSelect("toothpicks", "tooth", "Search Suggestions") -// Track conversion (item id, term, revenue) -constructorIO.trackConversion("an item id", "a search term", "45.00") +// Track when the user submits a search (searchTerm, originalQuery) +ConstructorIo.trackSearchSubmit("toothpicks", "tooth") ``` + +### Search Events + +These events should be sent manually by the consuming app. + +```kotlin +import io.constructor.core.ConstructorIo + +// Track when search results are loaded into view (searchTerm, resultCount) +ConstructorIo.trackSearchResultsLoaded("tooth", 789) + +// Track when a search result is clicked (itemName, customerId, searchTerm) +ConstructorIo.trackSearchResultClick("Fashionable Toothpicks", "1234567-AB", "tooth") + +// Track when a search result converts (itemName, customerId, revenue, searchTerm) +ConstructorIo.trackConversion("Fashionable Toothpicks", "1234567-AB", 12.99, "tooth") + +// Track when products are purchased (customerIds) +ConstructorIo.trackPurchase(customerIDs: ["123-AB", "456-CD"]) +``` \ No newline at end of file diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 6b5d0949..a0e9167b 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -22,9 +22,11 @@ class Constants { const val EVENT = "tr" const val API_KEY = "key" const val NUM_RESULTS = "num_results_" + const val CUSTOMER_ID = "customer_ids" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" const val USER_ID = "ui" + const val TERM_UNKNOWN = "TERM_UNKNOWN" } object QueryValues { diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 99ee9117..4286a04c 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -6,8 +6,8 @@ import io.constructor.data.ConstructorData import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.Group import io.constructor.data.model.Suggestion -import io.constructor.data.model.SuggestionViewModel import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -93,6 +93,10 @@ object ConstructorIo { } } + fun appMovedToForeground() { + preferenceHelper.getSessionId(sessionIncrementEventHandler) + } + fun getAutocompleteResults(query: String): Observable?>> { val params = mutableListOf>() configMemoryHolder.autocompleteResultCount?.entries?.forEach { @@ -101,19 +105,19 @@ object ConstructorIo { return dataManager.getAutocompleteResults(query, params.toTypedArray()) } - fun trackSelect(query: String, suggestion: SuggestionViewModel, errorCallback: ConstructorError = null) { + fun trackAutocompleteSelect(searchTerm: String, originalQuery: String, sectionName: String, group: Group? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() - suggestion.group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } - suggestion.group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - disposable.add(dataManager.trackSelect(suggestion.term, + group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } + group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } + disposable.add(dataManager.trackAutocompleteSelect(searchTerm, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to suggestion.section!!, - Constants.QueryConstants.ORIGINAL_QUERY to query, + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionName, + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_CLICK), encodedParams.toTypedArray()) .subscribe({ - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to query) + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) }, { t -> t.printStackTrace() errorCallback?.invoke(t) @@ -121,17 +125,17 @@ object ConstructorIo { })) } - fun trackSearch(query: String, suggestion: SuggestionViewModel, errorCallback: ConstructorError = null) { + fun trackSearchSubmit(searchTerm: String, originalQuery: String, group: Group?, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) val encodedParams: ArrayList> = arrayListOf() - suggestion.group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } - suggestion.group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } - disposable.add(dataManager.trackSearch(suggestion.term, + group?.groupId?.let { encodedParams.add(Constants.QueryConstants.GROUP_ID.urlEncode() to it) } + group?.displayName?.let { encodedParams.add(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode() to it.urlEncode()) } + disposable.add(dataManager.trackSearchSubmit(searchTerm, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.ORIGINAL_QUERY to query, + Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, Constants.QueryConstants.EVENT to Constants.QueryValues.EVENT_SEARCH), encodedParams.toTypedArray()) .subscribe({ - context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to query) + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to searchTerm) }, { it.printStackTrace() errorCallback?.invoke(it) @@ -139,11 +143,11 @@ object ConstructorIo { })) } - fun trackConversion(itemId: String, term: String = "TERM_UNKNOWN", revenue: String? = null, errorCallback: ConstructorError = null) { + fun trackConversion(itemName: String, customerId: String, revenue: Double?, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackConversion(term, itemId, revenue, + disposable.add(dataManager.trackConversion(searchTerm, itemName, customerId, "%.2f".format(revenue), arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to preferenceHelper.defaultItemSection)).subscribeOn(Schedulers.io()) + Constants.QueryConstants.AUTOCOMPLETE_SECTION to (sectionName ?: preferenceHelper.defaultItemSection))).subscribeOn(Schedulers.io()) .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) @@ -151,21 +155,22 @@ object ConstructorIo { })) } - fun trackSearchResultClickThrough(term: String, itemId: String, position: String? = null, errorCallback: ConstructorError = null) { + fun trackSearchResultClick(itemName: String, customerId: String, searchTerm: String = Constants.QueryConstants.TERM_UNKNOWN, sectionName: String? = null, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackSearchResultClickThrough(term, itemId, position, + val sName = sectionName ?: preferenceHelper.defaultItemSection + disposable.add(dataManager.trackSearchResultClick(itemName, customerId, searchTerm, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), - Constants.QueryConstants.AUTOCOMPLETE_SECTION to preferenceHelper.defaultItemSection)).subscribeOn(Schedulers.io()) + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sName)).subscribeOn(Schedulers.io()) .subscribe({}, { t -> t.printStackTrace() errorCallback?.invoke(t) - e("Conversion click through event error: ${t.message}") + e("Search result click event error: ${t.message}") })) } - fun trackSearchResultLoaded(term: String, resultCount: Int, errorCallback: ConstructorError = null) { + fun trackSearchResultsLoaded(term: String, resultCount: Int, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) - disposable.add(dataManager.trackSearchResultLoaded(term, resultCount, + disposable.add(dataManager.trackSearchResultsLoaded(term, resultCount, arrayOf(Constants.QueryConstants.SESSION to sessionId.toString(), Constants.QueryConstants.ACTION to Constants.QueryValues.EVENT_SEARCH_RESULTS)).subscribeOn(Schedulers.io()) .subscribe({}, { t -> @@ -187,4 +192,18 @@ object ConstructorIo { })) } + fun trackPurchase(clientIds: Array, sectionName: String? = null, errorCallback: ConstructorError = null) { + val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) + val sectionNameParam = sectionName ?: preferenceHelper.defaultItemSection + val params = mutableListOf(Constants.QueryConstants.SESSION to sessionId.toString(), + Constants.QueryConstants.AUTOCOMPLETE_SECTION to sectionNameParam) + clientIds.forEach { params.add(Constants.QueryConstants.CUSTOMER_ID to it) } + disposable.add(dataManager.trackPurchase(params.toTypedArray()).subscribeOn(Schedulers.io()) + .subscribe({}, { t -> + t.printStackTrace() + errorCallback?.invoke(t) + e("Input focus event error: ${t.message}") + })) + } + } \ 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 69836bca..d4582b01 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -25,32 +25,36 @@ constructor(private val constructorApi: ConstructorApi) { } }.toObservable() - fun trackSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { - return constructorApi.trackSelect(term, params.toMap(), encodedParams.toMap()) + fun trackAutocompleteSelect(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap()) } - fun trackSearch(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { - return constructorApi.trackSearch(term, params.toMap(), encodedParams.toMap()) + fun trackSearchSubmit(term: String, params: Array> = arrayOf(), encodedParams: Array> = arrayOf()): Completable { + return constructorApi.trackSearchSubmit(term, params.toMap(), encodedParams.toMap()) } fun trackSessionStart(params: Array>): Completable { return constructorApi.trackSessionStart(params.toMap()) } - fun trackConversion(term: String, itemId: String, revenue: String? = null, params: Array> = arrayOf()): Completable { - return constructorApi.trackConversion(term, itemId, revenue, params.toMap()) + fun trackConversion(term: String, itemName: String, customerId: String, revenue: String? = null, params: Array> = arrayOf()): Completable { + return constructorApi.trackConversion(term, itemName, customerId, revenue, params.toMap()) } - fun trackSearchResultClickThrough(term: String, itemId: String, position: String? = null, params: Array> = arrayOf()): Completable { - return constructorApi.trackSearchResultClickThrough(term, itemId, position, params.toMap()) + fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array> = arrayOf()): Completable { + return constructorApi.trackSearchResultTerm(term, itemName, customerId, params.toMap()) } - fun trackSearchResultLoaded(term: String, reultCount: Int, params: Array>): Completable { - return constructorApi.trackSearchResultLoaded(term, reultCount, params.toMap()) + fun trackSearchResultsLoaded(term: String, resultCount: Int, params: Array>): Completable { + return constructorApi.trackSearchResultsLoaded(term, resultCount, params.toMap()) } fun trackInputFocus(term: String?, params: Array>): Completable { return constructorApi.trackInputFocus(term, params.toMap()) } + fun trackPurchase(params: Array>): Completable { + return constructorApi.trackPurchase(params.toMap()) + } + } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt index 5c5aed59..c1ca2f5b 100755 --- a/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt +++ b/library/src/main/java/io/constructor/data/local/PreferencesHelper.kt @@ -30,13 +30,13 @@ constructor(@ApplicationContext context: Context, prefFileName: String = PREF_FI get() = preferences.getLong(SESSION_LAST_ACCESS, System.currentTimeMillis()) set(value) = preferences.edit().putLong(SESSION_LAST_ACCESS, value).apply() - fun getSessionId(sessionIncrementAction: ((String) -> Unit)? = null): Int { + fun getSessionId(sessionIncrementAction: ((String) -> Unit)? = null, forceIncrement: Boolean = false): Int { if (!preferences.contains(SESSION_ID)) { return resetSession(sessionIncrementAction) } val sessionTime = lastSessionAccess val timeDiff = System.currentTimeMillis() - sessionTime - if (timeDiff > SESSION_TIME_THRESHOLD) { + if (timeDiff > SESSION_TIME_THRESHOLD || forceIncrement) { var sessionId = preferences.getInt(SESSION_ID, 1) preferences.edit().putInt(SESSION_ID, ++sessionId).apply() sessionIncrementAction?.invoke(sessionId.toString()) 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 500fe72a..2264b226 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -2,11 +2,12 @@ package io.constructor.data.remote object ApiPaths { const val URL_GET_SUGGESTIONS = "autocomplete/{value}" - const val URL_SELECT_EVENT = "autocomplete/{term}/select" - const val URL_SEARCH_EVENT = "autocomplete/{term}/search" + const val URL_AUTOCOMPLETE_SELECT_EVENT = "autocomplete/{term}/select" + const val URL_SEARCH_SUBMIT_EVENT = "autocomplete/{term}/search" const val URL_SESSION_START_EVENT = "behavior" - const val URL_CONVERT_EVENT = "autocomplete/{term}/conversion" - const val URL_CLICK_THROUGH_EVENT = "autocomplete/{term}/click_through" + const val URL_CONVERSION_EVENT = "autocomplete/{term}/conversion" + const val URL_SEARCH_RESULT_CLICK_EVENT = "autocomplete/{term}/click_through" const val URL_BEHAVIOR = "behavior" + const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase" } \ 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 bf4df469..0a04d8ca 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -14,24 +14,27 @@ interface ConstructorApi { @GET(ApiPaths.URL_GET_SUGGESTIONS) fun getSuggestions(@Path("value") value: String, @QueryMap data: Map): Single> - @GET(ApiPaths.URL_SELECT_EVENT) - fun trackSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable + @GET(ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT) + fun trackAutocompleteSelect(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable - @GET(ApiPaths.URL_SEARCH_EVENT) - fun trackSearch(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable + @GET(ApiPaths.URL_SEARCH_SUBMIT_EVENT) + fun trackSearchSubmit(@Path("term") term: String, @QueryMap data: Map, @QueryMap(encoded = true) encodedData: Map): Completable @GET(ApiPaths.URL_SESSION_START_EVENT) fun trackSessionStart(@QueryMap params: Map): Completable - @GET(ApiPaths.URL_CONVERT_EVENT) - fun trackConversion(@Path("term") term: String, @Query("item_id") itemId: String, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable + @GET(ApiPaths.URL_CONVERSION_EVENT) + fun trackConversion(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @Query("revenue") revenue: String?, @QueryMap params: Map): Completable - @GET(ApiPaths.URL_CLICK_THROUGH_EVENT) - fun trackSearchResultClickThrough(@Path("term") term: String, @Query("item_id") itemId: String, @Query("position") position: String?, @QueryMap params: Map): Completable + @GET(ApiPaths.URL_SEARCH_RESULT_CLICK_EVENT) + fun trackSearchResultTerm(@Path("term") term: String, @Query("name") itemName: String, @Query("customer_id") customerId: String, @QueryMap params: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) - fun trackSearchResultLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable + fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map): Completable @GET(ApiPaths.URL_BEHAVIOR) fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map): Completable + + @GET(ApiPaths.URL_PURCHASE) + fun trackPurchase(@QueryMap params: Map): Completable } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/service/OnSearchService.kt b/library/src/main/java/io/constructor/service/OnSearchService.kt index 362657b7..23d6613e 100755 --- a/library/src/main/java/io/constructor/service/OnSearchService.kt +++ b/library/src/main/java/io/constructor/service/OnSearchService.kt @@ -18,11 +18,11 @@ class OnSearchService : IntentService("OnSearchService") { } } - override fun onHandleIntent(intent: Intent?) { - val query: String? = intent?.getStringExtra(Constants.EXTRA_QUERY) - val suggestion: SuggestionViewModel = intent?.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel + override fun onHandleIntent(intent: Intent) { + val query: String = intent.getStringExtra(Constants.EXTRA_QUERY) + val suggestion: SuggestionViewModel = intent.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel if (!suggestion.term.isBlank()) { - ConstructorIo.trackSearch(query!!, suggestion) + ConstructorIo.trackSearchSubmit(suggestion.term, query, suggestion.group) } } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/service/OnSelectService.kt b/library/src/main/java/io/constructor/service/OnSelectService.kt index 52715ec8..ab97e34e 100755 --- a/library/src/main/java/io/constructor/service/OnSelectService.kt +++ b/library/src/main/java/io/constructor/service/OnSelectService.kt @@ -22,11 +22,11 @@ class OnSelectService : IntentService("OnSelectService") { } - override fun onHandleIntent(intent: Intent?) { - val query: String? = intent?.getStringExtra(Constants.EXTRA_QUERY) - val suggestion: SuggestionViewModel = intent?.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel + override fun onHandleIntent(intent: Intent) { + val query: String = intent.getStringExtra(Constants.EXTRA_QUERY) + val suggestion: SuggestionViewModel = intent.getSerializableExtra(Constants.EXTRA_SUGGESTION) as SuggestionViewModel if (!suggestion.term.isBlank()) { - ConstructorIo.trackSelect(query!!, suggestion) + ConstructorIo.trackAutocompleteSelect(suggestion.term, query, suggestion.section!!, suggestion.group) } } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt index 05c7ed0d..a042b03f 100755 --- a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt +++ b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt @@ -1,13 +1,11 @@ package io.constructor.ui.suggestion import io.constructor.core.ConstructorIo -import io.constructor.data.ConstructorData import io.constructor.data.local.PreferencesHelper -import io.constructor.data.model.Suggestion import io.constructor.features.base.BasePresenter import io.constructor.injection.ConfigPersistent import io.constructor.util.d -import io.constructor.util.rx.scheduler.SchedulerUtils +import io.constructor.util.io2ui import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import java.util.concurrent.TimeUnit @@ -21,7 +19,7 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter()).subscribe({ query -> + disposables.add(mvpView.queryChanged().debounce(300, TimeUnit.MILLISECONDS).io2ui().subscribe({ query -> getSuggestions(query) }, {error -> error.printStackTrace() @@ -46,10 +44,11 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter?>>()).subscribe { data -> + disposables.add(ConstructorIo.getAutocompleteResults(text).io2ui().subscribe { data -> data.onValue { - ConstructorIo.trackSearchResultLoaded(text, it!!.size) - mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm) + it?.let { + mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm) + } } data.onError { if (it is NoSuchElementException) { diff --git a/library/src/main/java/io/constructor/util/Extensions.kt b/library/src/main/java/io/constructor/util/Extensions.kt index 076aefa2..43f7aa26 100755 --- a/library/src/main/java/io/constructor/util/Extensions.kt +++ b/library/src/main/java/io/constructor/util/Extensions.kt @@ -6,6 +6,9 @@ import android.os.Parcelable import android.support.v4.content.LocalBroadcastManager import android.util.Base64 import android.util.Log +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import java.io.Serializable import java.net.URLEncoder @@ -43,3 +46,9 @@ fun String.base64Encode(): String? { fun String.base64Decode(): String { return String(Base64.decode(this, Base64.NO_WRAP or Base64.NO_PADDING)) } + +fun Observable.io2ui(): Observable { + return compose { + it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + } +} diff --git a/library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt b/library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt deleted file mode 100755 index 2ad83e8d..00000000 --- a/library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.constructor.util.rx.scheduler - -/** - * Created by lam on 2/6/17. - */ - -object SchedulerUtils { - - fun ioToMain(): IoMainScheduler { - return IoMainScheduler() - } -} diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index b7656db5..1d8982b2 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -44,6 +44,7 @@ class ConstructorIoTest { every { pref.id } returns "1" every { pref.getSessionId() } returns 1 every { pref.getSessionId(any()) } returns 1 + every { pref.getSessionId(any(), any()) } returns 1 constructorIo.testInit(ctx, ConstructorIoConfig("dummyKey", testCells = listOf("1" to "2", "3" to "4")), data, pref, configMemoryHolder) } @@ -91,8 +92,8 @@ class ConstructorIoTest { } @Test - fun verifySessionStartUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=session_start&key=testKey&_dt=1520000000000" + fun verifySessionStartEventUrl() { + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=session_start&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -106,8 +107,8 @@ class ConstructorIoTest { } @Test - fun verifySearchClickThroughEvent() { - val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=cioand-0.1.0&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" + fun verifySearchResultClickEventUrl() { + val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=${BuildConfig.CLIENT_VERSION}&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("autocomplete") @@ -123,8 +124,8 @@ class ConstructorIoTest { } @Test - fun verifySearchLoadedEventUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=search-results&key=testKey&_dt=1520000000000" + fun verifySearchResultsLoadedEventUrl() { + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&s=1&action=search-results&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -139,7 +140,7 @@ class ConstructorIoTest { @Test fun verifyInputFocusEvent() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=${BuildConfig.CLIENT_VERSION}&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -154,31 +155,31 @@ class ConstructorIoTest { } @Test - fun trackSelectSuccess() { + fun trackAutocompleteSelectSuccess() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSelect(any(), any(), any()) } returns Completable.complete() - constructorIo.trackSelect("doggy dog", dummySuggestion) + every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.complete() + constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } } } @Test - fun trackSelectError() { + fun trackAutocompleteSelectError() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSelect(any(), any(), any()) } returns Completable.error(Exception()) - constructorIo.trackSelect("doggy dog", dummySuggestion) + every { data.trackAutocompleteSelect(any(), any(), any()) } returns Completable.error(Exception()) + constructorIo.trackAutocompleteSelect("doggy dog", "dog", "section1", dummySuggestion.group) verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } } } @Test - fun trackSearchSuccess() { + fun trackSearchSubmitSuccess() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSearch(any(), any(), any()) } returns Completable.complete() - constructorIo.trackSearch("doggy dog", dummySuggestion) + every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() + constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) verify(exactly = 1) { ctx.broadcastIntent(any(), any()) } } } @@ -225,11 +226,11 @@ class ConstructorIoTest { } @Test - fun trackSearchError() { + fun trackSearchSubmitError() { staticMockk("io.constructor.util.ExtensionsKt").use { every { ctx.broadcastIntent(any(), any()) } returns Unit - every { data.trackSearch(any(), any(), any()) } returns Completable.error(Exception()) - constructorIo.trackSearch("doggy dog", dummySuggestion) + every { data.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) + constructorIo.trackSearchSubmit("doggy dog", "dog", dummySuggestion.group) verify(exactly = 0) { ctx.broadcastIntent(any(), any()) } } } @@ -237,17 +238,17 @@ class ConstructorIoTest { @Test fun trackConversion() { every { pref.defaultItemSection } returns "Products" - every { data.trackConversion(any(), any(), any(), any()) } returns Completable.complete() - constructorIo.trackConversion(itemId = "1") - verify(exactly = 1) { data.trackConversion(any(), any(), any(), any()) } + every { data.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() + constructorIo.trackConversion("corn", "id1", 11.99) + verify(exactly = 1) { data.trackConversion("TERM_UNKNOWN", any(), any(), any(), any()) } } @Test - fun trackSearchResultClickThrough() { + fun trackSearchResultClick() { every { pref.defaultItemSection } returns "Products" - every { data.trackSearchResultClickThrough(any(), any(), any(), any()) } returns Completable.complete() - constructorIo.trackSearchResultClickThrough("1", "1") - verify(exactly = 1) { data.trackSearchResultClickThrough(any(), any(), any(), any()) } + every { data.trackSearchResultClick(any(), any(), any(), any()) } returns Completable.complete() + constructorIo.trackSearchResultClick("1", "1") + verify(exactly = 1) { data.trackSearchResultClick(any(), any(), any(), any()) } } @Test @@ -269,4 +270,12 @@ class ConstructorIoTest { verify(exactly = 1) { data.trackInputFocus(any(), any()) } } + @Test + fun trackPurchase() { + every { pref.defaultItemSection } returns "Products" + every { data.trackPurchase(any()) } returns Completable.complete() + constructorIo.trackPurchase(arrayOf("id1")) + verify(exactly = 1) { data.trackPurchase(any()) } + } + } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 9cf58c93..2ebfad37 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -72,37 +72,37 @@ class DataManagerTest { } @Test - fun trackSelect() { - every { constructorApi.trackSelect(any(), any(),any()) } returns Completable.complete() - dataManager.trackSelect("titanic") - verify(exactly = 1) { constructorApi.trackSelect(any(), any(), any())} + fun trackAutocompleteSelect() { + every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.complete() + dataManager.trackAutocompleteSelect("titanic") + verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} } @Test - fun trackSelectError() { - every { constructorApi.trackSelect(any(), any(),any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSelect("titanic").test() + fun trackAutocompleteSelectError() { + every { constructorApi.trackAutocompleteSelect(any(), any(),any()) } returns Completable.error(Exception()) + val observer = dataManager.trackAutocompleteSelect("titanic").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSelect(any(), any(), any())} + verify(exactly = 1) { constructorApi.trackAutocompleteSelect(any(), any(), any())} } @Test - fun trackSearch() { - every { constructorApi.trackSearch(any(), any(), any()) } returns Completable.complete() - dataManager.trackSearch("titanic") - verify(exactly = 1) { constructorApi.trackSearch(any(), any(), any())} + fun trackSearchSubmit() { + every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchSubmit("titanic") + verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} } @Test - fun trackSearchError() { - every { constructorApi.trackSearch(any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearch("titanic").test() + fun trackSearchSubmitError() { + every { constructorApi.trackSearchSubmit(any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchSubmit("titanic").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearch(any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchSubmit(any(), any(), any())} } @Test @@ -124,49 +124,49 @@ class DataManagerTest { @Test fun trackConversion() { - every { constructorApi.trackConversion(any(), any(), any(), any()) } returns Completable.complete() - dataManager.trackConversion("testTerm", "1") - verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any())} + every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.complete() + dataManager.trackConversion("testTerm", "item1", "id1", "11.99") + verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} } @Test fun trackConversionError() { - every { constructorApi.trackConversion(any(), any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackConversion("testTerm", "1").test() + every { constructorApi.trackConversion(any(), any(), any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackConversion("testTerm", "item1", "id1").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any())} + verify(exactly = 1) { constructorApi.trackConversion(any(), any(), any(), any(), any())} } @Test - fun trackSearchResultClickThrough() { - every { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchResultClickThrough("term", "1") - verify(exactly = 1) { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any())} + fun trackSearchResultClick() { + every { constructorApi.trackSearchResultTerm(any(), any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchResultClick("term", "id1", "term1") + verify(exactly = 1) { constructorApi.trackSearchResultTerm(any(), any(), any(), any())} } @Test - fun trackSearchResultClickThroughError() { - every { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearchResultClickThrough("term", "1").test() + fun trackSearchResultClickError() { + every { constructorApi.trackSearchResultTerm(any(), any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchResultClick("term", "1", "term1").test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearchResultClickThrough(any(), any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchResultTerm(any(), any(), any(), any())} } @Test - fun trackSearchResultLoaded() { - every { constructorApi.trackSearchResultLoaded(any(), any(), any()) } returns Completable.complete() - dataManager.trackSearchResultLoaded("term", 10, arrayOf()) - verify(exactly = 1) { constructorApi.trackSearchResultLoaded(any(), any(), any())} + fun trackSearchResultsLoaded() { + every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.complete() + dataManager.trackSearchResultsLoaded("term", 10, arrayOf()) + verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} } @Test - fun trackSearchResultLoadedError() { - every { constructorApi.trackSearchResultLoaded(any(), any(), any()) } returns Completable.error(Exception()) - val observer = dataManager.trackSearchResultLoaded("term", 10, arrayOf()).test() + fun trackSearchResultsLoadedError() { + every { constructorApi.trackSearchResultsLoaded(any(), any(), any()) } returns Completable.error(Exception()) + val observer = dataManager.trackSearchResultsLoaded("term", 10, arrayOf()).test() observer.assertError { true } - verify(exactly = 1) { constructorApi.trackSearchResultLoaded(any(), any(), any())} + verify(exactly = 1) { constructorApi.trackSearchResultsLoaded(any(), any(), any())} } @Test @@ -184,4 +184,11 @@ class DataManagerTest { verify(exactly = 1) { constructorApi.trackInputFocus(any(), any()) } } + @Test + fun trackPurchase() { + every { constructorApi.trackPurchase(any()) } returns Completable.complete() + dataManager.trackPurchase(arrayOf()) + verify(exactly = 1) { constructorApi.trackPurchase(any()) } + } + } \ No newline at end of file diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 6186b2d4..0a963b54 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -12,10 +12,11 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + ConstructorIo.appMovedToForeground() button.setOnClickListener { startActivity(Intent(this, SampleActivity::class.java)) } button2.setOnClickListener { startActivity(Intent(this, SampleActivityCustom::class.java)) } - button3.setOnClickListener { ConstructorIo.trackConversion("testId", revenue = "$11.99") } - button4.setOnClickListener { ConstructorIo.trackSearchResultClickThrough("testTerm", "testId", "1") } - button5.setOnClickListener { ConstructorIo.trackSearchResultLoaded("testTerm", Random().nextInt(99) + 1) } + button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.0) } + button4.setOnClickListener { ConstructorIo.trackSearchResultClick("testTerm", "testId", "1") } + button5.setOnClickListener { ConstructorIo.trackSearchResultsLoaded("testTerm", Random().nextInt(99) + 1) } } } diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 312e6efa..ea623bb5 100755 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -41,7 +41,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:text="Trigger SearchThrough" + android:text="Trigger Search Result Click" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button3" /> @@ -51,7 +51,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:text="Trigger SearchResult Loaded" + android:text="Trigger Search Result Loaded" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button4" />