Skip to content
Open
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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ Three types of these events exist:
4. **Recommendation Events** measure user interaction with recommendation results
5. **Quiz Events** measure user interaction with quiz results
6. **Conversion Events** measure user events like `add to cart` or `purchase`
7. **Agent Events** measure user interaction with the AI agent search assistant

### Autocomplete Events

Expand Down Expand Up @@ -672,3 +673,47 @@ ConstructorIo.trackResultsImpressionView(
searchTerm = "sneakers"
)
```

### Agent Search Assistant (ASA) Events

```kotlin
// Track when a user submits an intent to the agent (intent, sectionName?)
ConstructorIo.trackAgentSubmit("find me a red shirt")

// Track when agent results start loading (intent, sectionName?, intentResultId?)
ConstructorIo.trackAgentResultLoadStarted("find me a red shirt", intentResultId = "intent-123")

// Track when agent results finish loading (intent, searchResultCount, sectionName?, intentResultId?)
ConstructorIo.trackAgentResultLoadFinished("find me a red shirt", 25, intentResultId = "intent-123")

// Track when an agent search result is clicked (intent, searchResultId, itemId?, itemName?, variationId?, sectionName?, intentResultId?)
ConstructorIo.trackAgentResultClick(
intent = "find me a red shirt",
searchResultId = "result-123",
itemId = "item-456",
itemName = "Red Shirt",
variationId = "var-789",
intentResultId = "intent-123"
)

// Track when agent search results are viewed (intent, searchResultId, numResultsViewed, items?, sectionName?, intentResultId?)
val items = listOf(
ResultsImpressionItem("item-1", "Item One"),
ResultsImpressionItem("item-2", "Item Two", variationId = "var-2")
)
ConstructorIo.trackAgentResultView(
intent = "find me a red shirt",
searchResultId = "result-123",
numResultsViewed = 5,
items = items,
intentResultId = "intent-123"
)

// Track when a user submits a search from within the agent (intent, searchTerm, searchResultId, sectionName?, intentResultId?)
ConstructorIo.trackAgentSearchSubmit(
intent = "find me a red shirt",
searchTerm = "red shirt",
searchResultId = "result-123",
intentResultId = "intent-123"
)
```
282 changes: 278 additions & 4 deletions library/src/main/java/io/constructor/core/ConstructorIo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ import io.constructor.data.model.recommendations.RecommendationResultClickReques
import io.constructor.data.model.recommendations.RecommendationResultViewRequestBody
import io.constructor.data.model.recommendations.RecommendationsResponse
import io.constructor.data.model.search.*
import io.constructor.data.model.tracking.GenericResultClickRequestBody
import io.constructor.data.model.tracking.ItemDetailLoadRequestBody
import io.constructor.data.model.tracking.MediaImpressionRequestBody
import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody
import io.constructor.data.model.tracking.*
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
import io.constructor.injection.component.AppComponent
import io.constructor.injection.component.DaggerAppComponent
import io.constructor.injection.module.AppModule
Expand Down Expand Up @@ -2372,4 +2369,281 @@ object ConstructorIo {
arrayOf(Constants.QueryConstants.SECTION to section)
)
}

/**
* Tracks agent submit events.
*
* Example:
* ```
* ConstructorIo.trackAgentSubmit("find me a red shirt")
* ```
* @param intent the user's intent/query submitted to the agent
* @param sectionName the section name (defaults to "Products")
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentSubmit(intent: String, sectionName: String? = null, analyticsTags: Map<String, String>? = null) {
val completable = trackAgentSubmitInternal(intent, sectionName, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Inconsistent lambda style. The existing codebase uses two styles for the error lambda — either a single-line { t -> e("...") } or a multi-line block — but the new Agent methods split the parameter name t onto the next line after {, which isn't used elsewhere. Compare with the existing pattern at lines like 1253-1255:

disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
        t -> e("Quiz Result Click error: ${t.message}")
}))

The new code writes:

disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
    t -> e("Agent Submit error: ${t.message}")
}))

This is a minor inconsistency; prefer the single-line style used by the nearest neighbours (trackItemDetailLoaded, trackMediaImpressionView):

disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { t -> e("Agent Submit error: ${t.message}") }))

t -> e("Agent Submit error: ${t.message}")
}))
}

internal fun trackAgentSubmitInternal(intent: String, sectionName: String? = null, analyticsTags: Map<String, String>? = null): Completable {
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val requestBody = AgentSubmitRequestBody(
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
intent,
true,
section,
BuildConfig.CLIENT_VERSION,
preferenceHelper.id,
preferenceHelper.getSessionId(),
preferenceHelper.apiKey,
configMemoryHolder.userId,
configMemoryHolder.segments,
mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags),
System.currentTimeMillis()
)

return dataManager.trackAgentSubmit(
requestBody,
arrayOf(Constants.QueryConstants.SECTION to section)
)
}

/**
* Tracks agent result load started events.
*
* Example:
* ```
* ConstructorIo.trackAgentResultLoadStarted("find me a red shirt")
* ```
* @param intent the user's intent/query submitted to the agent
* @param sectionName the section name (defaults to "Products")
* @param intentResultId the intent result identifier
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentResultLoadStarted(intent: String, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null) {
val completable = trackAgentResultLoadStartedInternal(intent, sectionName, intentResultId, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
t -> e("Agent Result Load Started error: ${t.message}")
}))
}

internal fun trackAgentResultLoadStartedInternal(intent: String, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val requestBody = AgentResultLoadStartedRequestBody(
intent,
true,
section,
intentResultId,
BuildConfig.CLIENT_VERSION,
preferenceHelper.id,
preferenceHelper.getSessionId(),
preferenceHelper.apiKey,
configMemoryHolder.userId,
configMemoryHolder.segments,
mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags),
System.currentTimeMillis()
)

return dataManager.trackAgentResultLoadStarted(
requestBody,
arrayOf(Constants.QueryConstants.SECTION to section)
)
}

/**
* Tracks agent result load finished events.
*
* Example:
* ```
* ConstructorIo.trackAgentResultLoadFinished("find me a red shirt", 25)
* ```
* @param intent the user's intent/query submitted to the agent
* @param searchResultCount the number of search results returned
* @param sectionName the section name (defaults to "Products")
* @param intentResultId the intent result identifier
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentResultLoadFinished(intent: String, searchResultCount: Int, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null) {
val completable = trackAgentResultLoadFinishedInternal(intent, searchResultCount, sectionName, intentResultId, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
t -> e("Agent Result Load Finished error: ${t.message}")
}))
}

internal fun trackAgentResultLoadFinishedInternal(intent: String, searchResultCount: Int, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val requestBody = AgentResultLoadFinishedRequestBody(
intent,
searchResultCount,
true,
section,
intentResultId,
BuildConfig.CLIENT_VERSION,
preferenceHelper.id,
preferenceHelper.getSessionId(),
preferenceHelper.apiKey,
configMemoryHolder.userId,
configMemoryHolder.segments,
mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags),
System.currentTimeMillis()
)

return dataManager.trackAgentResultLoadFinished(
requestBody,
arrayOf(Constants.QueryConstants.SECTION to section)
)
}

/**
* Tracks agent result click events.
*
* Example:
* ```
* ConstructorIo.trackAgentResultClick("find me a red shirt", "result-123")
* ```
* @param intent the user's intent/query submitted to the agent
* @param searchResultId the identifier of the search result clicked
* @param itemId the item identifier
* @param itemName the item name
* @param variationId the variation identifier
* @param sectionName the section name (defaults to "Products")
* @param intentResultId the intent result identifier
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentResultClick(intent: String, searchResultId: String, itemId: String? = null, itemName: String? = null, variationId: String? = null, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null) {
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved
val completable = trackAgentResultClickInternal(intent, searchResultId, itemId, itemName, variationId, sectionName, intentResultId, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
t -> e("Agent Result Click error: ${t.message}")
}))
}

internal fun trackAgentResultClickInternal(intent: String, searchResultId: String, itemId: String? = null, itemName: String? = null, variationId: String? = null, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val requestBody = AgentResultClickRequestBody(
intent,
searchResultId,
itemId,
itemName,
variationId,
true,
section,
intentResultId,
BuildConfig.CLIENT_VERSION,
preferenceHelper.id,
preferenceHelper.getSessionId(),
preferenceHelper.apiKey,
configMemoryHolder.userId,
configMemoryHolder.segments,
mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags),
System.currentTimeMillis()
)

return dataManager.trackAgentResultClick(
requestBody,
arrayOf(Constants.QueryConstants.SECTION to section)
)
}

/**
* Tracks agent result view events.
*
* Example:
* ```
* ConstructorIo.trackAgentResultView("find me a red shirt", "result-123", 5)
* ```
* @param intent the user's intent/query submitted to the agent
* @param searchResultId the identifier of the search result
* @param numResultsViewed the number of results viewed
* @param items the list of items viewed (capped at 100)
* @param sectionName the section name (defaults to "Products")
* @param intentResultId the intent result identifier
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentResultView(intent: String, searchResultId: String, numResultsViewed: Int, items: List<ResultsImpressionItem>? = null, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Missing numResultsViewed validation. The analogous trackResultsImpressionView guards against an empty list (if (items.isEmpty()) return). Consider whether a numResultsViewed <= 0 should also be rejected or at least logged — particularly if the API treats it as invalid. Even a doc comment noting the expected range would be helpful for callers.

val completable = trackAgentResultViewInternal(intent, searchResultId, numResultsViewed, items, sectionName, intentResultId, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
t -> e("Agent Result View error: ${t.message}")
}))
}

internal fun trackAgentResultViewInternal(intent: String, searchResultId: String, numResultsViewed: Int, items: List<ResultsImpressionItem>? = null, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val cappedItems = items?.take(100)
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Magic number 100 should be extracted into a named constant. The existing Constants.kt doesn't define any numeric limits, so this introduces the first magic number for a cap. Define it clearly, e.g.:

private const val MAX_AGENT_ITEMS = 100
// ...
val cappedItems = items?.take(MAX_AGENT_ITEMS)

This matches the self-documenting style used throughout the rest of the codebase and makes the cap easy to change if the API spec evolves.

val requestBody = AgentResultViewRequestBody(
intent,
searchResultId,
numResultsViewed,
cappedItems,
true,
section,
intentResultId,
BuildConfig.CLIENT_VERSION,
preferenceHelper.id,
preferenceHelper.getSessionId(),
preferenceHelper.apiKey,
configMemoryHolder.userId,
configMemoryHolder.segments,
mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags),
System.currentTimeMillis()
)

return dataManager.trackAgentResultView(
requestBody,
arrayOf(Constants.QueryConstants.SECTION to section)
)
}

/**
* Tracks agent search submit events.
*
* Example:
* ```
* ConstructorIo.trackAgentSearchSubmit("find me a red shirt", "red shirt", "result-123")
* ```
* @param intent the user's intent/query submitted to the agent
* @param searchTerm the search term submitted
* @param searchResultId the identifier of the search result
* @param sectionName the section name (defaults to "Products")
* @param intentResultId the intent result identifier
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentSearchSubmit(intent: String, searchTerm: String, searchResultId: String, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null) {
val completable = trackAgentSearchSubmitInternal(intent, searchTerm, searchResultId, sectionName, intentResultId, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {
t -> e("Agent Search Submit error: ${t.message}")
}))
}

internal fun trackAgentSearchSubmitInternal(intent: String, searchTerm: String, searchResultId: String, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val requestBody = AgentSearchSubmitRequestBody(
intent,
searchTerm,
searchResultId,
true,
section,
intentResultId,
BuildConfig.CLIENT_VERSION,
preferenceHelper.id,
preferenceHelper.getSessionId(),
preferenceHelper.apiKey,
configMemoryHolder.userId,
configMemoryHolder.segments,
mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags),
System.currentTimeMillis()
)

return dataManager.trackAgentSearchSubmit(
requestBody,
arrayOf(Constants.QueryConstants.SECTION to section)
)
}
}
29 changes: 25 additions & 4 deletions library/src/main/java/io/constructor/data/DataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ import com.squareup.moshi.Moshi
import io.constructor.data.local.PreferencesHelper
import io.constructor.data.model.autocomplete.AutocompleteResponse
import io.constructor.data.model.browse.*
import io.constructor.data.model.tracking.ItemDetailLoadRequestBody
import io.constructor.data.model.conversion.ConversionRequestBody
import io.constructor.data.model.purchase.PurchaseRequestBody
import io.constructor.data.model.quiz.*
import io.constructor.data.model.recommendations.RecommendationResultClickRequestBody
import io.constructor.data.model.recommendations.RecommendationResultViewRequestBody
import io.constructor.data.model.recommendations.RecommendationsResponse
import io.constructor.data.model.search.*
import io.constructor.data.model.tracking.GenericResultClickRequestBody
import io.constructor.data.model.tracking.MediaImpressionRequestBody
import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody
import io.constructor.data.model.tracking.*
import io.constructor.data.remote.ApiPaths
import io.constructor.data.remote.ConstructorApi
import io.constructor.injection.ConstructorSdk
Expand Down Expand Up @@ -319,6 +316,30 @@ constructor(private val constructorApi: ConstructorApi, @ConstructorSdk private
return constructorApi.trackResultsImpressionView(resultsImpressionViewRequestBody, params.toMap())
}

fun trackAgentSubmit(body: AgentSubmitRequestBody, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAgentSubmit(body, params.toMap())
}

fun trackAgentResultLoadStarted(body: AgentResultLoadStartedRequestBody, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAgentResultLoadStarted(body, params.toMap())
}

fun trackAgentResultLoadFinished(body: AgentResultLoadFinishedRequestBody, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAgentResultLoadFinished(body, params.toMap())
}

fun trackAgentResultClick(body: AgentResultClickRequestBody, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAgentResultClick(body, params.toMap())
}

fun trackAgentResultView(body: AgentResultViewRequestBody, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAgentResultView(body, params.toMap())
}

fun trackAgentSearchSubmit(body: AgentSearchSubmitRequestBody, params: Array<Pair<String, String>> = arrayOf()): Completable {
return constructorApi.trackAgentSearchSubmit(body, params.toMap())
}

fun trackMediaImpressionView(preferencesHelper: PreferencesHelper, mediaImpressionRequestBody: MediaImpressionRequestBody): Completable {
val url = buildMediaUrl(preferencesHelper, ApiPaths.URL_MEDIA_IMPRESSION_VIEW_EVENT)
return constructorApi.trackMediaImpressionView(url, mediaImpressionRequestBody)
Expand Down
Loading
Loading