From 93ff681a7a7e3b7aae69f2803687be60e3e4de94 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Thu, 20 Sep 2018 19:36:15 -0700 Subject: [PATCH 01/10] Update README.md --- README.md | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 80d9df66..d3a2d983 100755 --- a/README.md +++ b/README.md @@ -1,34 +1,14 @@ -# Constructor.io Android Client Library +[![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) +# Constructor.io Android Client Library -An android client library for constructor.io suggestions engine +An Android Client library for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). # Usage -## 1a. Install using gradle -Add following to main gradle file: - -``` -allprojects { - repositories { - ... - maven { url 'https://jitpack.io' } - } -} -``` - -and then in your app gradle file include Constructor.IO dependency: - -``` -dependencies { - implementation 'com.github.Constructor-io:constructorio-client-android:v0.1' -} -``` - -## 1b. Manual import +## 1. Install -* Clone the android client repository from github ```git clone https://github.com/Constructor-io/constructorio-client-android.git``` -* Open and build the project in your IDE +Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.0.0) to add the client to your project. ## 2. Retrieve an autocomplete key From 395c38066359ebe455df15d2c668641d7b9c8e6a Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Thu, 27 Sep 2018 16:44:01 -0700 Subject: [PATCH 02/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d3a2d983..a8ab786c 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) +[![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) ![Android min](https://img.shields.io/badge/Android-4.4%2B-green.svg) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) # Constructor.io Android Client Library From a7170866a622bacb4369f8f6c6454e78d8f03bd1 Mon Sep 17 00:00:00 2001 From: Zubin Tiku Date: Wed, 3 Oct 2018 13:47:50 -0700 Subject: [PATCH 03/10] Ch2950/documentation (#19) * Documentation update --- README.md | 324 +++++++++++++++++++----------------------------------- 1 file changed, 112 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index a8ab786c..c2c0ded4 100755 --- a/README.md +++ b/README.md @@ -1,253 +1,153 @@ [![Release](https://jitpack.io/v/Constructor-io/constructorio-client-android.svg)](https://jitpack.io/#Constructor-io/constructorio--client-android) ![Android min](https://img.shields.io/badge/Android-4.4%2B-green.svg) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Constructor-io/constructorio-client-android/blob/master/LICENSE) -# Constructor.io Android Client Library +# Constructor.io Android Client -An Android Client library for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). - -# Usage +An Android Client for [Constructor.io](http://constructor.io/). [Constructor.io](http://constructor.io/) provides search as a service that optimizes results using artificial intelligence (including natural language processing, re-ranking to optimize for conversions, and user personalization). ## 1. Install Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.0.0) to add the client to your project. -## 2. Retrieve an autocomplete 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. +## 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. Init the Constructor.io Library +## 3. Implement the Autocomplete UI In your Application class add the following code with your key: +```kotlin +override fun onCreate() { + super.onCreate() + ConstructorIo.init(this, "YOUR API KEY") + + val fragment = supportFragmentManager.findFragmentById(R.id.fragment_suggestions) as SuggestionsFragment + fragment.setConstructorListener(object : ConstructorListener { + override fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?) { + Log.d(TAG, "onSuggestionSelected") + } + + override fun onQuerySentToServer(query: String) { + Log.d(TAG, "onQuerySentToServer") + } + + override fun onSuggestionsRetrieved(suggestions: List) { + Log.d(TAG, "onSuggestionsRetrieved") + } + + override fun onErrorGettingSuggestions(error: Throwable) { + Log.d(TAG, "handle network error getting suggestion") + } + }) +} ``` - override fun onCreate() { - super.onCreate() - ConstructorIo.init(this, "your-key") - } -``` -## 4a. Use default out-of-the-box UI - -To use the default, out-of-the-box UI, add the Sample Suggestions Fragment to your layout: - -```xml - -``` - -Skip to #5 if not customizing UI - -## 4b. Customize UI - -### Extend the suggestion screen fragment `BaseSuggestionFragment`. - -Implement the following: - -|Element name|Returned Type|Description| -|--|--|--| -|`layoutId`|`Int`|Returns your custom layout resource id for the fragment.| -|`getSuggestionsInputId`|`EditText`|Returns the id of the suggestion input field.| -|`getSuggestionListId`|`RecyclerView`|Returns your suggestion list id.| -|`getSuggestionAdapter`|Class that extends from `BaseSuggestionAdapter`|Returns your custom adapter.| -|`getProgressId`|Int`|Returns progress indicator id, used when request is being in progress. Return 0 for no progress| - -To see an example of usage, you can look at `SampleActivityCustom`. - -### Extend the suggestion items adapter `BaseSuggestionsAdapter`. - -Implement the following : - -|Element name|Returned Type|Description| -|--|--|--| -|`getItemLayoutId()`|`Int`|Returns your custom adapter item layout id for suggestion.| -|`getSuggestionNameId()`|`Int`|Return your text view id - the text will be the suggestion. | -|`getSuggestionGroupNameId()`|`Int`|Return your text view id - the text will be the suggestion group name, if found. | -|`onViewTypeSuggestion`|`String` text| Triggered when inflating an item which is a suggestion. Read below for more info.| -|`styleHighlightedSpans(spannable: Spannable, spanStart: Int, spanEnd: Int)`|`Unit`| Override to apply custom styling to highlighted part of suggestions search result. `spannable` is highlighted part of suggestion name, start and end mark position of the `spannable` within whole text.| - -abstract val styleHighlightedSpans: ((spannable: Spannable, spanStart: Int, spanEnd: Int) -> Unit)? - -In case you need to modify something in the ViewHolder (e.g make the group name bold) you can get a reference to it using `getHolder()` - -To see an example of usage, you can look at `SampleActivityCustom`. - -## 5. Get a reference to the `SuggestionsFragment` and add `ConstructorListener`: - -``` - val fragment = supportFragmentManager.findFragmentById(R.id.fragment_suggestions) as SuggestionsFragment - fragment.setConstructorListener(object : ConstructorListener { - override fun onSuggestionsRetrieved(suggestions: List) { - //got suggestions for a query - } - - override fun onQuerySentToServer(query: String) { - //request being made to server - } - - override fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?) { - //called when user taps on suggestion - } - - override fun onErrorGettingSuggestions(error: Throwable) { - //called when there is error getting suggestions - } - }) -``` -# Additional references -## Searching in Groups -Any data value can belong to a group. We will show the group name right below the item itself, if available. - -Let's remember the selection event. - -``` -fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?) -``` +### Selecting Results +To respond to a user selecting an autocomplete result, use the `onSuggestionSelected` method. If the autocomplete result has both a suggested term to search for and a group to search within (as in Apples in Juice Drinks), the group will be passed into the method. -Note `group` of type Group. It represents the group for an item and includes the following parameters: +### Performing Searches +To respond to a user performing a search (instead of selecting an autocomplete result), use the `onQuerySentToServer` method. -|Type|Name|Description| -|--|--|--| -|String|groupId|The group's id.| -|String|displayName|The group's display name.| -|String|path|The path to get more data for the group.| +## 4. Customize the Autocomplete UI -Let's say you search for 'apple' and the results are: +### Using the Default UI +To use the default, out-of-the-box UI, add the Sample Suggestions Fragment to your layout: +```xml + ``` -"suggestions": [ - { - "data": { - "groups": [ - { - "display_name": "food", - "group_id": "12", - "path": "/0/222/344" - }, - { - "display_name": "gadgets", - "group_id": "34", - "path": "/0/252/346/350" - } - ] - }, - "value": "apple" - } -] -``` - -We received two groups (food and gadgets) for our suggestion (apple). This means we'll have two suggestions in total: -1. 'apple' in group 'food' -2. 'apple' in group 'gadgets' -When the user taps on (1), term will be `apple` and group name will be `food`. +### Using a Custom UI -When the user taps on (2), term will be `apple` and group name will be `gadgets`. +To fully customize the UI, extend the `BaseSuggestionFragment` and the `BaseSuggestionsAdapter` -In other words, you can simply check whether the group property is null to find out if the user tapped on a search-in-group result: +```kotlin +class CustomSearchFragment : BaseSuggestionFragment() { -``` -fun onSuggestionSelected(term: String, group: Group?, autocompleteSection: String) { - if (group == null) { - // user tapped on an item - - } else { - // user tapped on a group + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + view?.backButton?.setOnClickListener { + view?.input?.text?.clear() + clearSuggestions() } + view?.searchButton?.setOnClickListener { triggerSearch() } } -``` - -## ConstructorListener Interface - -### onQuerySentToServer - -`onQuerySentToServer(query: String)` - -Triggered when the query is sent to the server. - -Parameter|Type|Description -|--|--|--| -`query`|String|The query made by the user. - -### onSuggestionSelected - -`onSuggestionSelected(term: String, group: Group?, autocompleteSection: String?)` - -Triggered when a suggestion is selected. - -|Parameter|Type|Description| -|--|--|--| -|`term`|String|The suggestion selected.| -|`group`|Group|Provides data on the group the selected term belongs to. Otherwise null.| -|`autocompleteSection`|String|The autocomplete section to which the selected term belongs (e.g "Search Suggestions", "Products"...)| -### onSuggestionsRetrieved -`onSuggestionsRetrieved(suggestions: List)` - -Triggered when the results for the query in question is retrieved. - -`suggestions` is a list of `Suggestion`s with the following parameters: - -|Parameter|Type|Description| -|--|--|--| -|`text`|String|The name of the suggestion.| -|`groups`|List|The top groups containing items that match for the query.| -|`matchedTerms`|List|matched terms within the query| -|`sectionName`|String|name of the section eg. "Search Suggestions", "Products"| - -### onErrorGettingSuggestions -`override fun onErrorGettingSuggestions(error: Throwable)` - -Triggered when error occured while requesting suggestions. - -|Parameter|Type|Description| -|--|--|--| -|`error`|Throwable|Exception thrown.| - -## BaseSuggestionFragment Abstract Class - -Default fragment expose two additional methods for easier implementing custom UI: - -### trackSearch() + // Return your custom adapter + override fun getSuggestionAdapter(): BaseSuggestionsAdapter { + return CustomSuggestionsAdapter() + } -Manually track search using text in the input box. + // Return your id of the suggestion input field + override fun getSuggestionsInputId(): Int { + return R.id.input + } -### clearSuggestions() + // Return your id of the suggestion list + override fun getSuggestionListId(): Int { + return R.id.suggestions + } -Clear input box and suggestion list. + // Return your progress indicator id, used when request is being in progress. Return 0 for no progress + override fun getProgressId(): Int { + return 0 + } + + // Return your custom layout resource id for the fragment + override fun layoutId(): Int { + return R.layout.fragment_custom_suggestions + } -## ConstructorIO public API +} -### trackConversion(term: String, itemId: String, revenue: String?) +class CustomSuggestionsAdapter() : BaseSuggestionsAdapter() { + + // Triggered when inflating an item which is a suggestion. + override fun onViewTypeSuggestion(holder: ViewHolder, suggestion: String, highlightedSuggestion: Spannable, groupName: String?) { + holder.suggestionName.text = highlightedSuggestion + val spans = highlightedSuggestion.getSpans(0, highlightedSuggestion.length, StyleSpan::class.java) + spans.forEach { highlightedSuggestion.setSpan(ForegroundColorSpan(Color.parseColor("#222222")), highlightedSuggestion.getSpanStart(it), highlightedSuggestion.getSpanEnd(it), 0) } + groupName?.let { holder.suggestionGroupName.text = holder.suggestionGroupName.context.getString(R.string.suggestion_group, it) } + } -Track conversion event + // Return your custom adapter item layout id for suggestion + override val itemLayoutId: Int + get() = R.layout.item_suggestion + + // Return your text view id - the text will be the suggestion. + override val suggestionNameId: Int + get() = R.id.suggestionName + + // Return your text view id - the text will be the suggestion group name, if present + override val suggestionGroupNameId: Int + get() = R.id.suggestionGroupName + +} +``` -|Parameter|Type|Description| -|--|--|--| -|`term`|String|Optional term for which tracking event is reported.| -|`itemId`|String|Id of item for which we want to trigger an event.| -|`revenue`|String|Optional revenue indicator.| +## 5. Instrument Behavioral Events -### trackSearchResultClickThrough(term: String, itemId: String, position: String?) +The Android Client sends behavioral events to [Constructor.io](http://constructor.io/) in order to continuously learn and improve results for future Autosuggest and Search requests. The Client only sends events in response to being called by the consuming app or in response to user interaction . For example, if the consuming app never calls the SDK code, no events will be sent. Besides the explicitly passed in event parameters, all user events contain a GUID based user ID that the client sets to identify the user as well as a session ID. -Track search result click event +Three types of these events exist: -|Parameter|Type|Description| -|--|--|--| -|`term`|String|Term used for search.| -|`itemId`|String|Id of item for which we want to track an event.| -|`position`|String|Optional position of clicked item on the list.| +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. **Search Events** measure user interaction with search results and the consuming app has to explicitly instrument them itself -### triggerSearchResultLoadedEvent(term: String, resultCount: Int) +```kotlin +import io.constructor.core.ConstructorIo -Track search results loaded event +// Track search results loaded (term, resultCount) +ConstructorIo.trackSearchResultLoaded("a search term", 123) -|Parameter|Type|Description| -|--|--|--| -|`term`|String|Term used for search.| -|`resultCount`|Int|Number of items found.| +// Track search result click (term, itemId, position) +ConstructorIo.trackSearchResultClickThrough("a search term", "an item id", "1") +// Track conversion (item id, term, revenue) +constructorIO.trackConversion("an item id", "a search term", "45.00") +``` From 7401340a8ec11e08ae5be6b422bdb58050fe32c6 Mon Sep 17 00:00:00 2001 From: qbasso Date: Mon, 22 Oct 2018 17:00:52 +0200 Subject: [PATCH 04/10] added sections number of results config parameters (#20) * added sections number of results config parameters * Fixed a few leftover references --- .../java/io/constructor/core/Constants.kt | 2 ++ .../java/io/constructor/core/ConstructorIo.kt | 19 +++++++++++-------- .../data/interceptor/TokenInterceptor.kt | 9 ++++++--- ...lMemoryHolder.kt => ConfigMemoryHolder.kt} | 3 ++- .../injection/component/AppComponent.kt | 4 ++-- .../constructor/injection/module/AppModule.kt | 6 +++--- .../injection/module/NetworkModule.kt | 4 ++-- .../io/constructor/core/ConstructorIoTest.kt | 15 ++++++++------- ...olderTest.kt => ConfigMemoryHolderTest.kt} | 10 +++++----- 9 files changed, 41 insertions(+), 31 deletions(-) rename library/src/main/java/io/constructor/data/memory/{TestCellMemoryHolder.kt => ConfigMemoryHolder.kt} (90%) rename library/src/test/java/io/constructor/data/memory/{TestCellMemoryHolderTest.kt => ConfigMemoryHolderTest.kt} (60%) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 71aae050..6a4ce316 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -21,6 +21,7 @@ class Constants { const val CLIENT = "c" const val EVENT = "tr" const val AUTOCOMPLETE_KEY = "autocomplete_key" + const val NUM_RESULTS = "num_results_" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" } @@ -30,6 +31,7 @@ class Constants { const val EVENT_SEARCH = "search" const val EVENT_SESSION_START = "session_start" const val SEARCH_SUGGESTIONS = "Search Suggestions" + const val PRODUCTS = "Products" const val EVENT_SEARCH_RESULTS = "search-results" const val EVENT_INPUT_FOCUS = "focus" } diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 4db42f34..605c8e41 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -5,7 +5,7 @@ import android.content.Context import io.constructor.BuildConfig import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.SuggestionViewModel import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent @@ -26,7 +26,7 @@ object ConstructorIo { private lateinit var dataManager: DataManager private lateinit var preferenceHelper: PreferencesHelper - private lateinit var testCellMemoryHolder: TestCellMemoryHolder + private lateinit var configMemoryHolder: ConfigMemoryHolder private lateinit var context: Context private var disposable = CompositeDisposable() @@ -49,15 +49,18 @@ object ConstructorIo { })) } - fun init(context: Context?, apiKey: String, defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION) { + fun init(context: Context?, apiKey: String, autocompleteResultCount: Map = mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, + Constants.QueryValues.PRODUCTS to 0), defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION) { if (context == null) { throw IllegalStateException("context is null, please init library using ConstructorIo.with(context)") } this.context = context.applicationContext dataManager = component.dataManager() preferenceHelper = component.preferenceHelper() - testCellMemoryHolder = component.testCellMemoryHolder() + configMemoryHolder = component.configMemoryHolder() + configMemoryHolder.autocompleteResultCount = autocompleteResultCount preferenceHelper.token = apiKey + preferenceHelper.defaultItemSection = defaultItemSection if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() @@ -69,21 +72,21 @@ object ConstructorIo { fun getClientId() = preferenceHelper.id fun setTestCellValues(pair1: Pair, pair2: Pair? = null, pair3: Pair? = null) { - testCellMemoryHolder.testCellParams = listOf(pair1, pair2, pair3) + configMemoryHolder.testCellParams = listOf(pair1, pair2, pair3) } fun clearTestCellValues() { - testCellMemoryHolder.testCellParams = emptyList() + configMemoryHolder.testCellParams = emptyList() } - internal fun testInit(context: Context?, apiKey: String, dataManager: DataManager, preferenceHelper: PreferencesHelper, testCellMemoryHolder: TestCellMemoryHolder) { + internal fun testInit(context: Context?, apiKey: String, dataManager: DataManager, preferenceHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder) { if (context == null) { throw IllegalStateException("Context is null, please init library using ConstructorIo.with(context)") } this.context = context.applicationContext this.dataManager = dataManager this.preferenceHelper = preferenceHelper - this.testCellMemoryHolder = testCellMemoryHolder + this.configMemoryHolder = configMemoryHolder preferenceHelper.token = apiKey if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index 42483aaa..584c02e1 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -4,12 +4,12 @@ import android.content.Context import io.constructor.BuildConfig import io.constructor.core.Constants import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import okhttp3.Interceptor import okhttp3.Response -class TokenInterceptor(val context: Context, private val preferencesHelper: PreferencesHelper, private val testCellMemoryHolder: TestCellMemoryHolder) : Interceptor { +class TokenInterceptor(val context: Context, private val preferencesHelper: PreferencesHelper, private val configMemoryHolder: ConfigMemoryHolder) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() @@ -18,11 +18,14 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref .addQueryParameter(Constants.QueryConstants.IDENTITY, preferencesHelper.id) .addQueryParameter(Constants.QueryConstants.TIMESTAMP, System.currentTimeMillis().toString()) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - testCellMemoryHolder.testCellParams.forEach { + configMemoryHolder.testCellParams.forEach { it?.let { builder.addQueryParameter(it.first, it.second) } } + configMemoryHolder.autocompleteResultCount?.entries?.forEach { + builder.addQueryParameter(Constants.QueryConstants.NUM_RESULTS+it.key, it.value.toString()) + } val url = builder.build() request = request.newBuilder().url(url).build() return chain.proceed(request) diff --git a/library/src/main/java/io/constructor/data/memory/TestCellMemoryHolder.kt b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt similarity index 90% rename from library/src/main/java/io/constructor/data/memory/TestCellMemoryHolder.kt rename to library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt index 481a9b93..71f516d1 100644 --- a/library/src/main/java/io/constructor/data/memory/TestCellMemoryHolder.kt +++ b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt @@ -4,7 +4,7 @@ import io.constructor.util.base64Decode import io.constructor.util.base64Encode import javax.inject.Inject -class TestCellMemoryHolder @Inject constructor() { +class ConfigMemoryHolder @Inject constructor() { private var backingString = "" @@ -32,4 +32,5 @@ class TestCellMemoryHolder @Inject constructor() { backingString = combined } + var autocompleteResultCount: Map? = null } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/injection/component/AppComponent.kt b/library/src/main/java/io/constructor/injection/component/AppComponent.kt index 78d715b0..e0b0be1b 100755 --- a/library/src/main/java/io/constructor/injection/component/AppComponent.kt +++ b/library/src/main/java/io/constructor/injection/component/AppComponent.kt @@ -4,7 +4,7 @@ import android.content.Context import dagger.Component import io.constructor.data.DataManager import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.remote.ConstructorApi import io.constructor.injection.ApplicationContext import io.constructor.injection.module.AppModule @@ -21,7 +21,7 @@ interface AppComponent { fun preferenceHelper(): PreferencesHelper - fun testCellMemoryHolder(): TestCellMemoryHolder + fun configMemoryHolder(): ConfigMemoryHolder fun constructorApi(): ConstructorApi } diff --git a/library/src/main/java/io/constructor/injection/module/AppModule.kt b/library/src/main/java/io/constructor/injection/module/AppModule.kt index 59765ab2..c48983ca 100755 --- a/library/src/main/java/io/constructor/injection/module/AppModule.kt +++ b/library/src/main/java/io/constructor/injection/module/AppModule.kt @@ -4,7 +4,7 @@ import android.content.Context import dagger.Module import dagger.Provides import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.injection.ApplicationContext import javax.inject.Singleton @@ -24,7 +24,7 @@ class AppModule(private val application: Context) { @Provides @Singleton - internal fun provideTestCellHolder(): TestCellMemoryHolder { - return TestCellMemoryHolder() + internal fun provideConfigMemoryHolder(): ConfigMemoryHolder { + return ConfigMemoryHolder() } } \ No newline at end of file diff --git a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt index ecd9388b..6f4dc333 100755 --- a/library/src/main/java/io/constructor/injection/module/NetworkModule.kt +++ b/library/src/main/java/io/constructor/injection/module/NetworkModule.kt @@ -8,7 +8,7 @@ import dagger.Provides import io.constructor.BuildConfig import io.constructor.data.interceptor.TokenInterceptor import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -50,7 +50,7 @@ class NetworkModule(private val context: Context) { @Provides @Singleton - internal fun provideTokenInterceptor(prefHelper: PreferencesHelper, testCellMemoryHolder: TestCellMemoryHolder): TokenInterceptor = TokenInterceptor(context, prefHelper, testCellMemoryHolder) + internal fun provideTokenInterceptor(prefHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder): TokenInterceptor = TokenInterceptor(context, prefHelper, configMemoryHolder) @Provides @Singleton diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index ec83b019..c0a0795f 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -5,7 +5,7 @@ import io.constructor.BuildConfig import io.constructor.data.DataManager import io.constructor.data.interceptor.TokenInterceptor import io.constructor.data.local.PreferencesHelper -import io.constructor.data.memory.TestCellMemoryHolder +import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.Group import io.constructor.data.model.SuggestionViewModel import io.constructor.util.RxSchedulersOverrideRule @@ -31,7 +31,7 @@ class ConstructorIoTest { private val ctx = mockk() private val pref = mockk() - private val testCellMemoryHolder = mockk() + private val configMemoryHolder = mockk() private val data = mockk() private var constructorIo = ConstructorIo private val sampleMillis = "1520000000000" @@ -44,7 +44,7 @@ class ConstructorIoTest { every { pref.id } returns "1" every { pref.getSessionId() } returns 1 every { pref.getSessionId(any()) } returns 1 - constructorIo.testInit(ctx, "dummyKey", data, pref, testCellMemoryHolder) + constructorIo.testInit(ctx, "dummyKey", data, pref, configMemoryHolder) } @After @@ -210,13 +210,14 @@ class ConstructorIoTest { val mockServer = MockWebServer() every { pref.token } returns "123" every { pref.id } returns "1" - every { testCellMemoryHolder.testCellParams = any() } just Runs - every { testCellMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") + every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) + every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") constructorIo.setTestCellValues("1" to "2", "3" to "4") - verify(exactly = 1) { testCellMemoryHolder.testCellParams = any() } + verify(exactly = 1) { configMemoryHolder.testCellParams = any() } mockServer.start() mockServer.enqueue(MockResponse()) - var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, testCellMemoryHolder)).build() + var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).build() client.newCall(Request.Builder().url(mockServer.url("/")).build()).execute() var recordedRequest = mockServer.takeRequest() assert(recordedRequest.path.contains("ef-1=2")) diff --git a/library/src/test/java/io/constructor/data/memory/TestCellMemoryHolderTest.kt b/library/src/test/java/io/constructor/data/memory/ConfigMemoryHolderTest.kt similarity index 60% rename from library/src/test/java/io/constructor/data/memory/TestCellMemoryHolderTest.kt rename to library/src/test/java/io/constructor/data/memory/ConfigMemoryHolderTest.kt index c6c83b3a..8d9c675e 100644 --- a/library/src/test/java/io/constructor/data/memory/TestCellMemoryHolderTest.kt +++ b/library/src/test/java/io/constructor/data/memory/ConfigMemoryHolderTest.kt @@ -6,19 +6,19 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class TestCellMemoryHolderTest { +class ConfigMemoryHolderTest { - private lateinit var testCellMemoryHolder: TestCellMemoryHolder + private lateinit var configMemoryHolder: ConfigMemoryHolder @Before fun setUp() { - testCellMemoryHolder = TestCellMemoryHolder() + configMemoryHolder = ConfigMemoryHolder() } @Test fun verifyTestCellsWrittenAndEncoded() { - testCellMemoryHolder.testCellParams = listOf("1" to "2", "3" to "4") - val params = testCellMemoryHolder.testCellParams + configMemoryHolder.testCellParams = listOf("1" to "2", "3" to "4") + val params = configMemoryHolder.testCellParams assert(params[0]!!.first == "ef-1" && params[0]!!.second == "2") assert(params[1]!!.first == "ef-3" && params[1]!!.second == "4") } From 761e22cb4b2d8ed799ef456b509f2c5efef42b8f Mon Sep 17 00:00:00 2001 From: qbasso Date: Thu, 25 Oct 2018 06:25:56 +0200 Subject: [PATCH 05/10] rename api_key (#22) --- .../java/io/constructor/core/Constants.kt | 2 +- .../data/interceptor/TokenInterceptor.kt | 2 +- .../io/constructor/core/ConstructorIoTest.kt | 28 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 6a4ce316..714ee902 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -20,7 +20,7 @@ class Constants { const val ORIGINAL_QUERY = "original_query" const val CLIENT = "c" const val EVENT = "tr" - const val AUTOCOMPLETE_KEY = "autocomplete_key" + const val API_KEY = "key" const val NUM_RESULTS = "num_results_" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index 584c02e1..fe9145f7 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -14,7 +14,7 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() val builder = request.url().newBuilder() - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, preferencesHelper.token) + .addQueryParameter(Constants.QueryConstants.API_KEY, preferencesHelper.token) .addQueryParameter(Constants.QueryConstants.IDENTITY, preferencesHelper.id) .addQueryParameter(Constants.QueryConstants.TIMESTAMP, System.currentTimeMillis().toString()) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index c0a0795f..45efdef0 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -53,7 +53,7 @@ class ConstructorIoTest { @Test fun verifySelectUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/select?s=1&i=1&_dt=1520000000000&autocomplete_section=Search%20Suggestions&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=click&c=cioand-${BuildConfig.VERSION_NAME}&autocomplete_key=testKey" + val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/select?s=1&i=1&_dt=1520000000000&autocomplete_section=Search%20Suggestions&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=click&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" val searchQuery = "dog" val term = "hot dogs" val urlBuilder = HttpUrl.Builder().scheme("https") @@ -70,20 +70,20 @@ class ConstructorIoTest { .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_CLICK) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) } @Test fun verifyGetSuggestionsUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/dog?autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/autocomplete/dog?key=testKey&_dt=1520000000000" val searchQuery = "dog" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("autocomplete") .addPathSegment(searchQuery) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -91,14 +91,14 @@ class ConstructorIoTest { @Test fun verifySessionStartUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=session_start&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=session_start&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.ACTION, "session_start") - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -106,7 +106,7 @@ 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&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/autocomplete/term/click_through?c=cioand-0.1.0&s=1&autocomplete_section=Products&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("autocomplete") @@ -115,7 +115,7 @@ class ConstructorIoTest { .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_SECTION, "Products") - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -123,14 +123,14 @@ class ConstructorIoTest { @Test fun verifySearchLoadedEventUrl() { - val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=search-results&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&s=1&action=search-results&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_SEARCH_RESULTS) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -138,7 +138,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&autocomplete_key=testKey&_dt=1520000000000" + val expected = "https://ac.cnstrc.com/behavior?c=cioand-0.1.0&i=user_id&s=1&action=focus&key=testKey&_dt=1520000000000" val urlBuilder = HttpUrl.Builder().scheme("https") .host("ac.cnstrc.com") .addPathSegment("behavior") @@ -146,7 +146,7 @@ class ConstructorIoTest { .addQueryParameter(Constants.QueryConstants.IDENTITY, "user_id") .addQueryParameter(Constants.QueryConstants.SESSION, "1") .addQueryParameter(Constants.QueryConstants.ACTION, Constants.QueryValues.EVENT_INPUT_FOCUS) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") .addQueryParameter(Constants.QueryConstants.TIMESTAMP, sampleMillis) val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) @@ -184,7 +184,7 @@ class ConstructorIoTest { @Test fun verifySearchUrl() { - val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/search?s=1&i=1&_dt=1520000000000&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=search&c=cioand-${BuildConfig.VERSION_NAME}&autocomplete_key=testKey" + val expected = "https://ac.cnstrc.com/autocomplete/hot%20dogs/search?s=1&i=1&_dt=1520000000000&original_query=dog&group%5Bgroup_id%5D=Meat%20%26%20Seafood&group%5Bdisplay_name%5D=Meat%20%26%20Seafood&tr=search&c=cioand-${BuildConfig.VERSION_NAME}&key=testKey" val originalQuery = "dog" val term = "hot dogs" val urlBuilder = HttpUrl.Builder().scheme("https") @@ -200,7 +200,7 @@ class ConstructorIoTest { .addEncodedQueryParameter(Constants.QueryConstants.GROUP_DISPLAY_NAME.urlEncode(), "Meat & Seafood".urlEncode()) .addQueryParameter(Constants.QueryConstants.EVENT, Constants.QueryValues.EVENT_SEARCH) .addQueryParameter(Constants.QueryConstants.CLIENT, BuildConfig.CLIENT_VERSION) - .addQueryParameter(Constants.QueryConstants.AUTOCOMPLETE_KEY, "testKey") + .addQueryParameter(Constants.QueryConstants.API_KEY, "testKey") val urlString = urlBuilder.build().url().toString() assertEquals(expected, urlString) } From a0712445362589ca897cb75bf8ed3b8770cea25c Mon Sep 17 00:00:00 2001 From: qbasso Date: Tue, 6 Nov 2018 00:14:56 +0100 Subject: [PATCH 06/10] added config object (#21) * added config object * use autocomplete result limits only in autocomplete calls --- .../java/io/constructor/core/ConstructorIo.kt | 34 +++++++++---------- .../constructor/core/ConstructorIoConfig.kt | 8 +++++ .../java/io/constructor/data/DataManager.kt | 2 +- .../data/interceptor/TokenInterceptor.kt | 3 -- .../constructor/data/remote/ConstructorApi.kt | 2 +- .../io/constructor/core/ConstructorIoTest.kt | 5 ++- .../io/constructor/data/DataManagerTest.kt | 11 +++--- .../java/io/constructor/sample/SampleApp.kt | 5 +-- 8 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 library/src/main/java/io/constructor/core/ConstructorIoConfig.kt diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 605c8e41..44e98f21 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -2,10 +2,11 @@ package io.constructor.core import android.annotation.SuppressLint import android.content.Context -import io.constructor.BuildConfig +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.Suggestion import io.constructor.data.model.SuggestionViewModel import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent @@ -15,6 +16,7 @@ import io.constructor.util.broadcastIntent import io.constructor.util.d import io.constructor.util.e import io.constructor.util.urlEncode +import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers import java.util.* @@ -49,8 +51,7 @@ object ConstructorIo { })) } - fun init(context: Context?, apiKey: String, autocompleteResultCount: Map = mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, - Constants.QueryValues.PRODUCTS to 0), defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION) { + fun init(context: Context?, constructorIoConfig: ConstructorIoConfig) { if (context == null) { throw IllegalStateException("context is null, please init library using ConstructorIo.with(context)") } @@ -58,10 +59,11 @@ object ConstructorIo { dataManager = component.dataManager() preferenceHelper = component.preferenceHelper() configMemoryHolder = component.configMemoryHolder() - configMemoryHolder.autocompleteResultCount = autocompleteResultCount - preferenceHelper.token = apiKey + configMemoryHolder.autocompleteResultCount = constructorIoConfig.autocompleteResultCount + configMemoryHolder.testCellParams = constructorIoConfig.testCells + preferenceHelper.token = constructorIoConfig.apiKey - preferenceHelper.defaultItemSection = defaultItemSection + preferenceHelper.defaultItemSection = constructorIoConfig.defaultItemSection if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() } @@ -71,15 +73,7 @@ object ConstructorIo { fun getClientId() = preferenceHelper.id - fun setTestCellValues(pair1: Pair, pair2: Pair? = null, pair3: Pair? = null) { - configMemoryHolder.testCellParams = listOf(pair1, pair2, pair3) - } - - fun clearTestCellValues() { - configMemoryHolder.testCellParams = emptyList() - } - - internal fun testInit(context: Context?, apiKey: String, dataManager: DataManager, preferenceHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder) { + internal fun testInit(context: Context?, constructorIoConfig: ConstructorIoConfig, dataManager: DataManager, preferenceHelper: PreferencesHelper, configMemoryHolder: ConfigMemoryHolder) { if (context == null) { throw IllegalStateException("Context is null, please init library using ConstructorIo.with(context)") } @@ -87,13 +81,19 @@ object ConstructorIo { this.dataManager = dataManager this.preferenceHelper = preferenceHelper this.configMemoryHolder = configMemoryHolder - preferenceHelper.token = apiKey + preferenceHelper.token = constructorIoConfig.apiKey if (preferenceHelper.id.isBlank()) { preferenceHelper.id = UUID.randomUUID().toString() } } - fun getAutocompleteResults(query: String) = dataManager.getAutocompleteResults(query) + fun getAutocompleteResults(query: String): Observable?>> { + val params = mutableListOf>() + configMemoryHolder.autocompleteResultCount?.entries?.forEach { + params.add(Pair(Constants.QueryConstants.NUM_RESULTS+it.key, it.value.toString())) + } + return dataManager.getAutocompleteResults(query, params.toTypedArray()) + } fun trackSelect(query: String, suggestion: SuggestionViewModel, errorCallback: ConstructorError = null) { val sessionId = preferenceHelper.getSessionId(sessionIncrementEventHandler) diff --git a/library/src/main/java/io/constructor/core/ConstructorIoConfig.kt b/library/src/main/java/io/constructor/core/ConstructorIoConfig.kt new file mode 100644 index 00000000..61356839 --- /dev/null +++ b/library/src/main/java/io/constructor/core/ConstructorIoConfig.kt @@ -0,0 +1,8 @@ +package io.constructor.core + +import io.constructor.BuildConfig + +data class ConstructorIoConfig(val apiKey: String, + val autocompleteResultCount: Map = mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0), + val defaultItemSection: String = BuildConfig.AUTOCOMPLETE_SECTION, + val testCells: List> = emptyList()) \ 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 947f2bb0..69836bca 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton class DataManager @Inject constructor(private val constructorApi: ConstructorApi) { - fun getAutocompleteResults(text: String): Observable?>> = constructorApi.getSuggestions(text).map { + fun getAutocompleteResults(text: String, params: Array> = arrayOf()): Observable?>> = constructorApi.getSuggestions(text, params.toMap()).map { if (!it.isError) { it.response()?.let { if (it.isSuccessful) { diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index fe9145f7..fe2151f0 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -23,9 +23,6 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref builder.addQueryParameter(it.first, it.second) } } - configMemoryHolder.autocompleteResultCount?.entries?.forEach { - builder.addQueryParameter(Constants.QueryConstants.NUM_RESULTS+it.key, it.value.toString()) - } val url = builder.build() request = request.newBuilder().url(url).build() return chain.proceed(request) 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 b3ccb8a6..bf4df469 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -12,7 +12,7 @@ import retrofit2.http.QueryMap interface ConstructorApi { @GET(ApiPaths.URL_GET_SUGGESTIONS) - fun getSuggestions(@Path("value") value: String): Single> + 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 diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 45efdef0..949d884c 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -44,7 +44,8 @@ class ConstructorIoTest { every { pref.id } returns "1" every { pref.getSessionId() } returns 1 every { pref.getSessionId(any()) } returns 1 - constructorIo.testInit(ctx, "dummyKey", data, pref, configMemoryHolder) + constructorIo.testInit(ctx, ConstructorIoConfig("dummyKey", + testCells = listOf("1" to "2", "3" to "4")), data, pref, configMemoryHolder) } @After @@ -213,8 +214,6 @@ class ConstructorIoTest { every { configMemoryHolder.testCellParams = any() } just Runs every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") - constructorIo.setTestCellValues("1" to "2", "3" to "4") - verify(exactly = 1) { configMemoryHolder.testCellParams = any() } mockServer.start() mockServer.enqueue(MockResponse()) var client = OkHttpClient.Builder().addInterceptor(TokenInterceptor(ctx, pref, configMemoryHolder)).build() diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index 530efc43..9cf58c93 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -27,7 +27,7 @@ class DataManagerTest { @Test fun getSuggestions() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -36,7 +36,7 @@ class DataManagerTest { @Test fun getSuggestionsBadServerResponse() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.error(500, ResponseBody.create(MediaType.parse("text/plain"), "Error")))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.networkError @@ -45,7 +45,7 @@ class DataManagerTest { @Test fun getSuggestionsException() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.error(Exception())) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.error(Exception())) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.isError @@ -54,7 +54,7 @@ class DataManagerTest { @Test fun getSuggestionsUnexpectedDataResponse() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) + every { constructorApi.getSuggestions("titanic", any()) } returns Single.just(Result.response(Response.success(TestDataLoader.loadResponseWithUnexpectedData()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.get()!!.isNotEmpty() && it.get()!!.size == 5 @@ -63,7 +63,8 @@ class DataManagerTest { @Test fun getSuggestionsEmptyResponse() { - every { constructorApi.getSuggestions("titanic") } returns Single.just(Result.response(Response.success(TestDataLoader.loadEmptyResponse()))) + every { constructorApi.getSuggestions("titanic", any() + ) } returns Single.just(Result.response(Response.success(TestDataLoader.loadEmptyResponse()))) val observer = dataManager.getAutocompleteResults("titanic").test() observer.assertComplete().assertValue { it.isEmpty diff --git a/sample/src/main/java/io/constructor/sample/SampleApp.kt b/sample/src/main/java/io/constructor/sample/SampleApp.kt index 724ff37b..5bfacc5c 100755 --- a/sample/src/main/java/io/constructor/sample/SampleApp.kt +++ b/sample/src/main/java/io/constructor/sample/SampleApp.kt @@ -2,12 +2,13 @@ package io.constructor.sample import android.app.Application import io.constructor.core.ConstructorIo +import io.constructor.core.ConstructorIoConfig class SampleApp : Application() { override fun onCreate() { super.onCreate() - ConstructorIo.init(this, "key_OucJxxrfiTVUQx0C") - ConstructorIo.setTestCellValues("ab" to "cd", "11" to "22") + ConstructorIo.init(this, ConstructorIoConfig("key_OucJxxrfiTVUQx0C", + testCells = listOf("ab" to "cd", "11" to "22"))) } } \ No newline at end of file From 73e0de059192203648523a8aadffc341b04e307e Mon Sep 17 00:00:00 2001 From: qbasso Date: Sat, 12 Jan 2019 18:58:30 +0100 Subject: [PATCH 07/10] ch3426 add uid parameter (#23) --- library/src/main/java/io/constructor/core/Constants.kt | 1 + library/src/main/java/io/constructor/core/ConstructorIo.kt | 6 ++++++ .../io/constructor/data/interceptor/TokenInterceptor.kt | 3 +++ .../java/io/constructor/data/memory/ConfigMemoryHolder.kt | 2 ++ .../src/test/java/io/constructor/core/ConstructorIoTest.kt | 2 ++ sample/src/main/java/io/constructor/sample/SampleApp.kt | 1 + 6 files changed, 15 insertions(+) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 714ee902..6b5d0949 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -24,6 +24,7 @@ class Constants { const val NUM_RESULTS = "num_results_" const val GROUP_ID = "group[group_id]" const val GROUP_DISPLAY_NAME = "group[display_name]" + const val USER_ID = "ui" } 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 44e98f21..99ee9117 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -32,6 +32,12 @@ object ConstructorIo { private lateinit var context: Context private var disposable = CompositeDisposable() + var userId: String? + get() = configMemoryHolder.userId + set(value) { + configMemoryHolder.userId = value + } + internal val component: AppComponent by lazy { DaggerAppComponent.builder() .appModule(AppModule(context)) diff --git a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt index fe2151f0..98e5cc09 100755 --- a/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt +++ b/library/src/main/java/io/constructor/data/interceptor/TokenInterceptor.kt @@ -23,6 +23,9 @@ class TokenInterceptor(val context: Context, private val preferencesHelper: Pref builder.addQueryParameter(it.first, it.second) } } + configMemoryHolder.userId?.let { + builder.addQueryParameter(Constants.QueryConstants.USER_ID, it) + } val url = builder.build() request = request.newBuilder().url(url).build() return chain.proceed(request) diff --git a/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt index 71f516d1..67b89d86 100644 --- a/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt +++ b/library/src/main/java/io/constructor/data/memory/ConfigMemoryHolder.kt @@ -33,4 +33,6 @@ class ConfigMemoryHolder @Inject constructor() { } var autocompleteResultCount: Map? = null + + var userId: String? = null } \ No newline at end of file diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index 949d884c..b7656db5 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -212,6 +212,7 @@ class ConstructorIoTest { every { pref.token } returns "123" every { pref.id } returns "1" every { configMemoryHolder.testCellParams = any() } just Runs + every { configMemoryHolder.userId } returns "uid" every { configMemoryHolder.autocompleteResultCount } returns mapOf(Constants.QueryValues.SEARCH_SUGGESTIONS to 10, Constants.QueryValues.PRODUCTS to 0) every { configMemoryHolder.testCellParams } returns listOf("ef-1" to "2", "ef-3" to "4") mockServer.start() @@ -220,6 +221,7 @@ class ConstructorIoTest { client.newCall(Request.Builder().url(mockServer.url("/")).build()).execute() var recordedRequest = mockServer.takeRequest() assert(recordedRequest.path.contains("ef-1=2")) + assert(recordedRequest.path.contains("ui=uid")) } @Test diff --git a/sample/src/main/java/io/constructor/sample/SampleApp.kt b/sample/src/main/java/io/constructor/sample/SampleApp.kt index 5bfacc5c..03a9ff29 100755 --- a/sample/src/main/java/io/constructor/sample/SampleApp.kt +++ b/sample/src/main/java/io/constructor/sample/SampleApp.kt @@ -10,5 +10,6 @@ class SampleApp : Application() { super.onCreate() ConstructorIo.init(this, ConstructorIoConfig("key_OucJxxrfiTVUQx0C", testCells = listOf("ab" to "cd", "11" to "22"))) + ConstructorIo.userId = "uid" } } \ No newline at end of file From 541a0d5a976f933f77679e1744936f37a7a3c9ef Mon Sep 17 00:00:00 2001 From: qbasso Date: Thu, 31 Jan 2019 06:55:44 +0100 Subject: [PATCH 08/10] ch4501 added purchase track event (#25) --- .../main/java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 14 ++++++++++++++ .../main/java/io/constructor/data/DataManager.kt | 8 ++++++-- .../java/io/constructor/data/remote/ApiPaths.kt | 1 + .../io/constructor/data/remote/ConstructorApi.kt | 3 +++ .../io/constructor/core/ConstructorIoTest.kt | 16 ++++++++++++---- .../java/io/constructor/data/DataManagerTest.kt | 7 +++++++ 7 files changed, 44 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index 6b5d0949..6acba189 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -22,6 +22,7 @@ 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" diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 99ee9117..34e7bdce 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -187,4 +187,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..ffb575a1 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -45,12 +45,16 @@ constructor(private val constructorApi: ConstructorApi) { return constructorApi.trackSearchResultClickThrough(term, itemId, position, params.toMap()) } - fun trackSearchResultLoaded(term: String, reultCount: Int, params: Array>): Completable { - return constructorApi.trackSearchResultLoaded(term, reultCount, params.toMap()) + fun trackSearchResultLoaded(term: String, resultCount: Int, params: Array>): Completable { + return constructorApi.trackSearchResultLoaded(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/remote/ApiPaths.kt b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt index 500fe72a..f4cf1baa 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -8,5 +8,6 @@ object ApiPaths { const val URL_CONVERT_EVENT = "autocomplete/{term}/conversion" const val URL_CLICK_THROUGH_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..348e9ffc 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -34,4 +34,7 @@ interface ConstructorApi { @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/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index b7656db5..cc7dcedf 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -92,7 +92,7 @@ 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" + 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") @@ -107,7 +107,7 @@ 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" + 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") @@ -124,7 +124,7 @@ 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" + 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 +139,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") @@ -269,4 +269,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..e787ca27 100755 --- a/library/src/test/java/io/constructor/data/DataManagerTest.kt +++ b/library/src/test/java/io/constructor/data/DataManagerTest.kt @@ -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 From 8a56a9aea593bdf23b1d4c92e3b2fbe57f7f2c1a Mon Sep 17 00:00:00 2001 From: qbasso Date: Wed, 6 Feb 2019 08:41:42 +0100 Subject: [PATCH 09/10] ch3226 match tracking calls singatures to iOS calls (#26) * ch3226 match tracking calls signatures to iOS calls * Fixed naming conventions * Updated README.md --- README.md | 38 ++++++++-- .../java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 47 ++++++------ .../java/io/constructor/data/DataManager.kt | 20 ++--- .../io/constructor/data/remote/ApiPaths.kt | 8 +- .../constructor/data/remote/ConstructorApi.kt | 18 ++--- .../io/constructor/service/OnSearchService.kt | 8 +- .../io/constructor/service/OnSelectService.kt | 8 +- .../ui/suggestion/SuggestionsPresenter.kt | 2 +- .../io/constructor/core/ConstructorIoTest.kt | 44 +++++------ .../io/constructor/data/DataManagerTest.kt | 76 +++++++++---------- .../io/constructor/sample/MainActivity.kt | 6 +- sample/src/main/res/layout/activity_main.xml | 4 +- 13 files changed, 153 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index c2c0ded4..f5386118 100755 --- a/README.md +++ b/README.md @@ -136,18 +136,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 6acba189..a0e9167b 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -26,6 +26,7 @@ class Constants { 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 34e7bdce..d0b87aa6 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 @@ -101,19 +101,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 +121,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 +139,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 +151,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 -> diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index ffb575a1..d4582b01 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -25,28 +25,28 @@ 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, resultCount: Int, params: Array>): Completable { - return constructorApi.trackSearchResultLoaded(term, resultCount, 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 { 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 f4cf1baa..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,11 @@ 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" 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 348e9ffc..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,23 +14,23 @@ 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 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..0bdfecd5 100755 --- a/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt +++ b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt @@ -48,7 +48,7 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter?>>()).subscribe { data -> data.onValue { - ConstructorIo.trackSearchResultLoaded(text, it!!.size) + ConstructorIo.trackSearchResultsLoaded(text, it!!.size) mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm) } data.onError { diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt index cc7dcedf..f88c845d 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTest.kt @@ -91,7 +91,7 @@ class ConstructorIoTest { } @Test - fun verifySessionStartUrl() { + 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") @@ -106,7 +106,7 @@ class ConstructorIoTest { } @Test - fun verifySearchClickThroughEvent() { + 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") @@ -123,7 +123,7 @@ class ConstructorIoTest { } @Test - fun verifySearchLoadedEventUrl() { + 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") @@ -154,31 +154,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 +225,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 +237,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 diff --git a/library/src/test/java/io/constructor/data/DataManagerTest.kt b/library/src/test/java/io/constructor/data/DataManagerTest.kt index e787ca27..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 diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 6186b2d4..61a7a5a5 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -14,8 +14,8 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) 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.99) } + 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" /> From 5689e525469239470c21447739ac1d1313a30465 Mon Sep 17 00:00:00 2001 From: qbasso Date: Thu, 21 Feb 2019 06:26:34 +0100 Subject: [PATCH 10/10] ch3225 foreground method added (#27) * ch3225 foreground method added * Removed force session internal in foreground call (but kept signature) --- .../main/java/io/constructor/core/ConstructorIo.kt | 4 ++++ .../io/constructor/data/local/PreferencesHelper.kt | 4 ++-- .../ui/suggestion/SuggestionsPresenter.kt | 13 ++++++------- .../src/main/java/io/constructor/util/Extensions.kt | 9 +++++++++ .../constructor/util/rx/scheduler/SchedulerUtils.kt | 12 ------------ .../java/io/constructor/core/ConstructorIoTest.kt | 1 + .../main/java/io/constructor/sample/MainActivity.kt | 3 ++- 7 files changed, 24 insertions(+), 22 deletions(-) delete mode 100755 library/src/main/java/io/constructor/util/rx/scheduler/SchedulerUtils.kt diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index d0b87aa6..4286a04c 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -93,6 +93,10 @@ object ConstructorIo { } } + fun appMovedToForeground() { + preferenceHelper.getSessionId(sessionIncrementEventHandler) + } + fun getAutocompleteResults(query: String): Observable?>> { val params = mutableListOf>() configMemoryHolder.autocompleteResultCount?.entries?.forEach { 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/ui/suggestion/SuggestionsPresenter.kt b/library/src/main/java/io/constructor/ui/suggestion/SuggestionsPresenter.kt index 0bdfecd5..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.trackSearchResultsLoaded(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 f88c845d..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) } diff --git a/sample/src/main/java/io/constructor/sample/MainActivity.kt b/sample/src/main/java/io/constructor/sample/MainActivity.kt index 61a7a5a5..0a963b54 100755 --- a/sample/src/main/java/io/constructor/sample/MainActivity.kt +++ b/sample/src/main/java/io/constructor/sample/MainActivity.kt @@ -12,9 +12,10 @@ 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", "id", 11.99) } + button3.setOnClickListener { ConstructorIo.trackConversion("testId", "id", 11.0) } button4.setOnClickListener { ConstructorIo.trackSearchResultClick("testTerm", "testId", "1") } button5.setOnClickListener { ConstructorIo.trackSearchResultsLoaded("testTerm", Random().nextInt(99) + 1) } }