Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
```
1 change: 1 addition & 0 deletions library/src/main/java/io/constructor/core/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 24 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 @@ -101,71 +101,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 Down
20 changes: 10 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,28 +25,28 @@ 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, resultCount: Int, params: Array<Pair<String, String>>): Completable {
return constructorApi.trackSearchResultLoaded(term, resultCount, 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 {
Expand Down
8 changes: 4 additions & 4 deletions library/src/main/java/io/constructor/data/remote/ApiPaths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ constructor(private val preferencesHelper: PreferencesHelper) : BasePresenter<Su
}
disposables.add(ConstructorIo.getAutocompleteResults(text).compose(SchedulerUtils.ioToMain<ConstructorData<List<Suggestion>?>>()).subscribe { data ->
data.onValue {
ConstructorIo.trackSearchResultLoaded(text, it!!.size)
ConstructorIo.trackSearchResultsLoaded(text, it!!.size)
mvpView.showSuggestions(it, preferencesHelper.groupsShownForFirstTerm)
}
data.onError {
Expand Down
Loading