Skip to content
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ An Android Client for [Constructor.io](http://constructor.io/). [Constructor.io
Please follow the directions at [Jitpack.io](https://jitpack.io/#Constructor-io/constructorio-client-android/v1.1.0) to add the client to your project.

## 2. Retrieve an API key

You can find this in your [Constructor.io dashboard](https://constructor.io/dashboard). Contact sales if you'd like to sign up, or support if you believe your company already has an account.

## 3. Implement the Autocomplete UI
Expand Down Expand Up @@ -136,18 +137,42 @@ The Android Client sends behavioral events to [Constructor.io](http://constructo
Three types of these events exist:

1. **General Events** are sent as needed when an instance of the Client is created or initialized
1. **Autocomplete Events** measure user interaction with autocomplete results and the `CIOAutocompleteViewController` sends them automatically.
1. **Autocomplete Events** measure user interaction with autocomplete results and extending from `BaseSuggestionFragment` sends them automatically.
1. **Search Events** measure user interaction with search results and the consuming app has to explicitly instrument them itself

### Autocomplete Events

If you decide to extend from the `BaseSuggestionFragment`, these events are sent automatically.

```kotlin
import io.constructor.core.ConstructorIo

// Track search results loaded (term, resultCount)
ConstructorIo.trackSearchResultLoaded("a search term", 123)
// Track when the user focuses into the search bar (searchTerm)
ConstructorIo.trackInputFocus("")

// Track search result click (term, itemId, position)
ConstructorIo.trackSearchResultClickThrough("a search term", "an item id", "1")
// Track when the user selects an autocomplete suggestion (searchTerm, originalQuery, sectionName)
ConstructorIo.trackAutocompleteSelect("toothpicks", "tooth", "Search Suggestions")

// Track conversion (item id, term, revenue)
constructorIO.trackConversion("an item id", "a search term", "45.00")
// Track when the user submits a search (searchTerm, originalQuery)
ConstructorIo.trackSearchSubmit("toothpicks", "tooth")
```

### Search Events

These events should be sent manually by the consuming app.

```kotlin
import io.constructor.core.ConstructorIo

// Track when search results are loaded into view (searchTerm, resultCount)
ConstructorIo.trackSearchResultsLoaded("tooth", 789)

// Track when a search result is clicked (itemName, customerId, searchTerm)
ConstructorIo.trackSearchResultClick("Fashionable Toothpicks", "1234567-AB", "tooth")

// Track when a search result converts (itemName, customerId, revenue, searchTerm)
ConstructorIo.trackConversion("Fashionable Toothpicks", "1234567-AB", 12.99, "tooth")

// Track when products are purchased (customerIds)
ConstructorIo.trackPurchase(customerIDs: ["123-AB", "456-CD"])
```
2 changes: 2 additions & 0 deletions library/src/main/java/io/constructor/core/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ class Constants {
const val EVENT = "tr"
const val API_KEY = "key"
const val NUM_RESULTS = "num_results_"
const val CUSTOMER_ID = "customer_ids"
const val GROUP_ID = "group[group_id]"
const val GROUP_DISPLAY_NAME = "group[display_name]"
const val USER_ID = "ui"
const val TERM_UNKNOWN = "TERM_UNKNOWN"
}

object QueryValues {
Expand Down
65 changes: 42 additions & 23 deletions library/src/main/java/io/constructor/core/ConstructorIo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,6 +93,10 @@ object ConstructorIo {
}
}

fun appMovedToForeground() {
preferenceHelper.getSessionId(sessionIncrementEventHandler)
}

fun getAutocompleteResults(query: String): Observable<ConstructorData<List<Suggestion>?>> {
val params = mutableListOf<Pair<String, String>>()
configMemoryHolder.autocompleteResultCount?.entries?.forEach {
Expand All @@ -101,71 +105,72 @@ 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<Pair<String, String>> = 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)
e("trigger select error: ${t.message}") //To change body of created functions use File | Settings | File Templates.
}))
}

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<Pair<String, String>> = 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)
e("trigger search error: ${it.message}")
}))
}

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)
e("Conversion event error: ${t.message}")
}))
}

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 ->
Expand All @@ -187,4 +192,18 @@ object ConstructorIo {
}))
}

fun trackPurchase(clientIds: Array<String>, 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}")
}))
}

}
24 changes: 14 additions & 10 deletions library/src/main/java/io/constructor/data/DataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,36 @@ constructor(private val constructorApi: ConstructorApi) {
}
}.toObservable()

fun trackSelect(term: String, params: Array<Pair<String, String>> = arrayOf(), encodedParams: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackSelect(term, params.toMap(), encodedParams.toMap())
fun trackAutocompleteSelect(term: String, params: Array<Pair<String, String>> = arrayOf(), encodedParams: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAutocompleteSelect(term, params.toMap(), encodedParams.toMap())
}

fun trackSearch(term: String, params: Array<Pair<String, String>> = arrayOf(), encodedParams: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackSearch(term, params.toMap(), encodedParams.toMap())
fun trackSearchSubmit(term: String, params: Array<Pair<String, String>> = arrayOf(), encodedParams: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackSearchSubmit(term, params.toMap(), encodedParams.toMap())
}

fun trackSessionStart(params: Array<Pair<String, String>>): Completable {
return constructorApi.trackSessionStart(params.toMap())
}

fun trackConversion(term: String, itemId: String, revenue: String? = null, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackConversion(term, itemId, revenue, params.toMap())
fun trackConversion(term: String, itemName: String, customerId: String, revenue: String? = null, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackConversion(term, itemName, customerId, revenue, params.toMap())
}

fun trackSearchResultClickThrough(term: String, itemId: String, position: String? = null, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackSearchResultClickThrough(term, itemId, position, params.toMap())
fun trackSearchResultClick(itemName: String, customerId: String, term: String, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackSearchResultTerm(term, itemName, customerId, params.toMap())
}

fun trackSearchResultLoaded(term: String, reultCount: Int, params: Array<Pair<String, String>>): Completable {
return constructorApi.trackSearchResultLoaded(term, reultCount, params.toMap())
fun trackSearchResultsLoaded(term: String, resultCount: Int, params: Array<Pair<String, String>>): Completable {
return constructorApi.trackSearchResultsLoaded(term, resultCount, params.toMap())
}

fun trackInputFocus(term: String?, params: Array<Pair<String, String>>): Completable {
return constructorApi.trackInputFocus(term, params.toMap())
}

fun trackPurchase(params: Array<Pair<String, String>>): Completable {
return constructorApi.trackPurchase(params.toMap())
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package io.constructor.data.remote

object ApiPaths {
const val URL_GET_SUGGESTIONS = "autocomplete/{value}"
const val URL_SELECT_EVENT = "autocomplete/{term}/select"
const val URL_SEARCH_EVENT = "autocomplete/{term}/search"
const val URL_AUTOCOMPLETE_SELECT_EVENT = "autocomplete/{term}/select"
const val URL_SEARCH_SUBMIT_EVENT = "autocomplete/{term}/search"
const val URL_SESSION_START_EVENT = "behavior"
const val URL_CONVERT_EVENT = "autocomplete/{term}/conversion"
const val URL_CLICK_THROUGH_EVENT = "autocomplete/{term}/click_through"
const val URL_CONVERSION_EVENT = "autocomplete/{term}/conversion"
const val URL_SEARCH_RESULT_CLICK_EVENT = "autocomplete/{term}/click_through"
const val URL_BEHAVIOR = "behavior"
const val URL_PURCHASE = "autocomplete/TERM_UNKNOWN/purchase"

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,27 @@ interface ConstructorApi {
@GET(ApiPaths.URL_GET_SUGGESTIONS)
fun getSuggestions(@Path("value") value: String, @QueryMap data: Map<String, String>): Single<Result<AutocompleteResult>>

@GET(ApiPaths.URL_SELECT_EVENT)
fun trackSelect(@Path("term") term: String, @QueryMap data: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable
@GET(ApiPaths.URL_AUTOCOMPLETE_SELECT_EVENT)
fun trackAutocompleteSelect(@Path("term") term: String, @QueryMap data: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable

@GET(ApiPaths.URL_SEARCH_EVENT)
fun trackSearch(@Path("term") term: String, @QueryMap data: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable
@GET(ApiPaths.URL_SEARCH_SUBMIT_EVENT)
fun trackSearchSubmit(@Path("term") term: String, @QueryMap data: Map<String, String>, @QueryMap(encoded = true) encodedData: Map<String, String>): Completable

@GET(ApiPaths.URL_SESSION_START_EVENT)
fun trackSessionStart(@QueryMap params: Map<String, String>): Completable

@GET(ApiPaths.URL_CONVERT_EVENT)
fun trackConversion(@Path("term") term: String, @Query("item_id") itemId: String, @Query("revenue") revenue: String?, @QueryMap params: Map<String, String>): 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<String, String>): 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<String, String>): 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<String, String>): Completable

@GET(ApiPaths.URL_BEHAVIOR)
fun trackSearchResultLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map<String, String>): Completable
fun trackSearchResultsLoaded(@Query("term") term: String, @Query("num_results") resultCount: Int, @QueryMap params: Map<String, String>): Completable

@GET(ApiPaths.URL_BEHAVIOR)
fun trackInputFocus(@Query("term") term: String?, @QueryMap params: Map<String, String>): Completable

@GET(ApiPaths.URL_PURCHASE)
fun trackPurchase(@QueryMap params: Map<String, String>): Completable
}
Loading