diff --git a/README.md b/README.md index b248383f..121e27bd 100755 --- a/README.md +++ b/README.md @@ -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. **Product Insights Agent Events** measure user interaction with the Product Insights Agent ### Autocomplete Events @@ -679,3 +680,79 @@ ConstructorIo.trackResultsImpressionView( searchTerm = "sneakers" ) ``` + +### Product Insights Agent Events + +```kotlin +// Track when the user focuses on the Product Insights Agent (itemId) +ConstructorIo.trackProductInsightsAgentFocus( + ProductInsightsAgentFocusTrackingData.build("1234567-AB") { + itemName = "Waterproof Jacket" + } +) + +// Track when the Product Insights Agent scrolls out of view (itemId) +ConstructorIo.trackProductInsightsAgentOutOfView( + ProductInsightsAgentOutOfViewTrackingData.build("1234567-AB") +) + +// Track when the Product Insights Agent comes into view (itemId, questions) +ConstructorIo.trackProductInsightsAgentView( + ProductInsightsAgentViewTrackingData.build( + "1234567-AB", + listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability")) + ) +) + +// Track the spans of time the Product Insights Agent was in view (itemId, questions, viewTimespans) +// Timespan start and end are epoch milliseconds, i.e. System.currentTimeMillis(), and are sent as ISO-8601 UTC timestamps +ConstructorIo.trackProductInsightsAgentViews( + ProductInsightsAgentViewsTrackingData.build( + "1234567-AB", + listOf(ProductInsightsAgentQuestion("Is it waterproof?")), + listOf(ProductInsightsAgentViewTimespan(viewStartedAtMillis, System.currentTimeMillis())) + ) +) + +// Track when the user clicks a suggested question (itemId, question) +ConstructorIo.trackProductInsightsAgentQuestionClick( + ProductInsightsAgentQuestionClickTrackingData.build("1234567-AB", "Is it waterproof?") { + questionTopic = "Durability" + } +) + +// Track when the user submits a question (itemId, question) +ConstructorIo.trackProductInsightsAgentQuestionSubmit( + ProductInsightsAgentQuestionSubmitTrackingData.build("1234567-AB", "Is it waterproof?") { + threadId = "thread-123" + } +) + +// Track when an answer is shown to the user (itemId, question, answerText) +// Only the first 100 items and follow-up questions are sent, and answerText over 2000 Unicode code points is truncated to fit +ConstructorIo.trackProductInsightsAgentAnswerView( + ProductInsightsAgentAnswerViewTrackingData.build("1234567-AB", "Is it waterproof?", "Yes, it is fully waterproof.") { + qnaResultId = "qna-result-123" + items = listOf(TrackingItem("7654321-BA", null, null, null)) + followUpQuestions = listOf("Is it breathable?") + } +) + +// Track when the user gives feedback on an answer (itemId, feedbackLabel) +ConstructorIo.trackProductInsightsAgentAnswerFeedback( + ProductInsightsAgentAnswerFeedbackTrackingData.build("1234567-AB", ProductInsightsAgentFeedbackLabel.THUMBS_UP) { + qnaResultId = "qna-result-123" + } +) + +// Track when the user clicks an item recommended in an answer (itemId of the clicked item, question, seedItemId of the item the agent is displayed for) +ConstructorIo.trackProductInsightsAgentResultClick( + ProductInsightsAgentResultClickTrackingData.build("7654321-BA", "Is it waterproof?", "1234567-AB") { + position = 0 + qnaResultId = "qna-result-123" + } +) + +// All Product Insights Agent requests also accept sectionName, threadId, itemName, variationId and analyticsTags +// Request level analytics tags are merged with the default analytics tags passed on initialization +``` diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index c161da67..dce477c3 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -26,12 +26,22 @@ 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.ProductInsightsAgentAnswerFeedbackRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentAnswerViewRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentFollowUpQuestion +import io.constructor.data.model.tracking.ProductInsightsAgentItemRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentQuestionRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentResultClickRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewTimespanBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewsRequestBody import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule import io.constructor.util.broadcastIntent import io.constructor.util.e +import io.constructor.util.truncateToCodePoints import io.constructor.util.urlEncode import io.reactivex.Completable import io.reactivex.Observable @@ -40,6 +50,7 @@ import io.reactivex.exceptions.UndeliverableException import io.reactivex.plugins.RxJavaPlugins import io.reactivex.schedulers.Schedulers import java.io.IOException +import java.text.SimpleDateFormat import java.util.* typealias ConstructorError = ((Throwable) -> Unit)? @@ -360,6 +371,20 @@ object ConstructorIo { return analyticsTags } + // SimpleDateFormat isn't thread-safe and tracking runs on Schedulers.io(), so create one per call + // Section and analytics tags are resolved the same way for every Product Insights Agent event + private fun ProductInsightsAgentTrackingData.resolvedSection(): String = + sectionName ?: preferenceHelper.defaultItemSection + + private fun ProductInsightsAgentTrackingData.mergedAnalyticsTags(): Map? = + mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags) + + private fun formatIsoTimestamp(epochMillis: Long): String { + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + format.timeZone = TimeZone.getTimeZone("UTC") + return format.format(Date(epochMillis)) + } + /** * Returns a list of autocomplete suggestions. * @@ -2430,4 +2455,388 @@ object ConstructorIo { arrayOf(Constants.QueryConstants.SECTION to section) ) } + + /** + * Tracks Product Insights Agent focus events. + * + * Example: + * ``` + * val request = ProductInsightsAgentFocusTrackingData.build("1234567-AB") { + * itemName = "Waterproof Jacket" + * } + * ConstructorIo.trackProductInsightsAgentFocus(request) + * ``` + * @param request the Product Insights Agent focus request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentFocus(request: ProductInsightsAgentFocusTrackingData) { + val completable = trackProductInsightsAgentFocusInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Focus error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentFocusInternal(request: ProductInsightsAgentFocusTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentItemRequestBody( + itemId = request.itemId, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentFocus( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent out of view events. + * + * Example: + * ``` + * val request = ProductInsightsAgentOutOfViewTrackingData.build("1234567-AB") { + * itemName = "Waterproof Jacket" + * } + * ConstructorIo.trackProductInsightsAgentOutOfView(request) + * ``` + * @param request the Product Insights Agent out of view request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentOutOfView(request: ProductInsightsAgentOutOfViewTrackingData) { + val completable = trackProductInsightsAgentOutOfViewInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Out Of View error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentOutOfViewInternal(request: ProductInsightsAgentOutOfViewTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentItemRequestBody( + itemId = request.itemId, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentOutOfView( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent view events. + * + * Example: + * ``` + * val request = ProductInsightsAgentViewTrackingData.build( + * "1234567-AB", + * listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability")) + * ) { + * itemName = "Waterproof Jacket" + * } + * ConstructorIo.trackProductInsightsAgentView(request) + * ``` + * @param request the Product Insights Agent view request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentView(request: ProductInsightsAgentViewTrackingData) { + val completable = trackProductInsightsAgentViewInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent View error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentViewInternal(request: ProductInsightsAgentViewTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentViewRequestBody( + itemId = request.itemId, + questions = request.questions, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentView( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent views events. + * + * Example: + * ``` + * val request = ProductInsightsAgentViewsTrackingData.build( + * "1234567-AB", + * listOf(ProductInsightsAgentQuestion("Is it waterproof?")), + * listOf(ProductInsightsAgentViewTimespan(viewStartedAtMillis, System.currentTimeMillis())) + * ) { + * itemName = "Waterproof Jacket" + * } + * ConstructorIo.trackProductInsightsAgentViews(request) + * ``` + * @param request the Product Insights Agent views request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentViews(request: ProductInsightsAgentViewsTrackingData) { + val completable = trackProductInsightsAgentViewsInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Views error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentViewsInternal(request: ProductInsightsAgentViewsTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentViewsRequestBody( + itemId = request.itemId, + questions = request.questions, + viewTimespans = request.viewTimespans.map { + ProductInsightsAgentViewTimespanBody(formatIsoTimestamp(it.start), formatIsoTimestamp(it.end)) + }, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentViews( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent question click events. + * + * Example: + * ``` + * val request = ProductInsightsAgentQuestionClickTrackingData.build("1234567-AB", "Is it waterproof?") { + * questionTopic = "Durability" + * } + * ConstructorIo.trackProductInsightsAgentQuestionClick(request) + * ``` + * @param request the Product Insights Agent question click request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentQuestionClick(request: ProductInsightsAgentQuestionClickTrackingData) { + val completable = trackProductInsightsAgentQuestionClickInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Question Click error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentQuestionClickInternal(request: ProductInsightsAgentQuestionClickTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentQuestionRequestBody( + itemId = request.itemId, + question = request.question, + questionTopic = request.questionTopic, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentQuestionClick( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent question submit events. + * + * Example: + * ``` + * val request = ProductInsightsAgentQuestionSubmitTrackingData.build("1234567-AB", "Is it waterproof?") { + * threadId = "thread-123" + * } + * ConstructorIo.trackProductInsightsAgentQuestionSubmit(request) + * ``` + * @param request the Product Insights Agent question submit request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentQuestionSubmit(request: ProductInsightsAgentQuestionSubmitTrackingData) { + val completable = trackProductInsightsAgentQuestionSubmitInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Question Submit error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentQuestionSubmitInternal(request: ProductInsightsAgentQuestionSubmitTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentQuestionRequestBody( + itemId = request.itemId, + question = request.question, + questionTopic = request.questionTopic, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentQuestionSubmit( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent answer feedback events. + * + * Example: + * ``` + * val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("1234567-AB", ProductInsightsAgentFeedbackLabel.THUMBS_UP) { + * qnaResultId = "qna-result-123" + * } + * ConstructorIo.trackProductInsightsAgentAnswerFeedback(request) + * ``` + * @param request the Product Insights Agent answer feedback request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentAnswerFeedback(request: ProductInsightsAgentAnswerFeedbackTrackingData) { + val completable = trackProductInsightsAgentAnswerFeedbackInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Answer Feedback error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentAnswerFeedbackInternal(request: ProductInsightsAgentAnswerFeedbackTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentAnswerFeedbackRequestBody( + itemId = request.itemId, + feedbackLabel = request.feedbackLabel, + qnaResultId = request.qnaResultId, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentAnswerFeedback( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent result click events. + * + * Example: + * ``` + * val request = ProductInsightsAgentResultClickTrackingData.build("rec-123", "Is it waterproof?", "1234567-AB") { + * position = 0 + * qnaResultId = "qna-result-123" + * } + * ConstructorIo.trackProductInsightsAgentResultClick(request) + * ``` + * @param request the Product Insights Agent result click request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentResultClick(request: ProductInsightsAgentResultClickTrackingData) { + val completable = trackProductInsightsAgentResultClickInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Result Click error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentResultClickInternal(request: ProductInsightsAgentResultClickTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentResultClickRequestBody( + itemId = request.itemId, + question = request.question, + seedItemId = request.seedItemId, + position = request.position, + seedItemName = request.seedItemName, + seedVariationId = request.seedVariationId, + qnaResultId = request.qnaResultId, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentResultClick( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } + + /** + * Tracks Product Insights Agent answer view events. + * + * Example: + * ``` + * val request = ProductInsightsAgentAnswerViewTrackingData.build("1234567-AB", "Is it waterproof?", "Yes, it is fully waterproof.") { + * qnaResultId = "qna-result-123" + * items = listOf(TrackingItem("rec-123", null, null, null)) + * followUpQuestions = listOf("Is it breathable?") + * } + * ConstructorIo.trackProductInsightsAgentAnswerView(request) + * ``` + * @param request the Product Insights Agent answer view request object holding all the tracking parameters. Any + * analytics tags set on it are merged with the default analytics tags set on + * [ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on + * key collision. + */ + fun trackProductInsightsAgentAnswerView(request: ProductInsightsAgentAnswerViewTrackingData) { + val completable = trackProductInsightsAgentAnswerViewInternal(request) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { + t -> e("Product Insights Agent Answer View error: ${t.message}") + })) + } + + internal fun trackProductInsightsAgentAnswerViewInternal(request: ProductInsightsAgentAnswerViewTrackingData): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val section = request.resolvedSection() + val requestBody = ProductInsightsAgentAnswerViewRequestBody( + itemId = request.itemId, + question = request.question, + answerText = request.answerText.truncateToCodePoints(2000), + questionTopic = request.questionTopic, + qnaResultId = request.qnaResultId, + items = request.items?.take(100), + followUpQuestions = request.followUpQuestions?.take(100)?.map { ProductInsightsAgentFollowUpQuestion(it) }, + itemName = request.itemName, + variationId = request.variationId, + threadId = request.threadId, + analyticsTags = request.mergedAnalyticsTags() + ) + + return dataManager.trackProductInsightsAgentAnswerView( + requestBody, + arrayOf(Constants.QueryConstants.SECTION to section) + ) + } } diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 07f54dbe..ea158c95 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -4,7 +4,7 @@ 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.tracking.GenericResultClickRequestBody import io.constructor.data.model.conversion.ConversionRequestBody import io.constructor.data.model.purchase.PurchaseRequestBody import io.constructor.data.model.quiz.* @@ -12,8 +12,15 @@ 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.ProductInsightsAgentAnswerFeedbackRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentAnswerViewRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentItemRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentQuestionRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentResultClickRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewsRequestBody import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody import io.constructor.data.remote.ApiPaths import io.constructor.data.remote.ConstructorApi @@ -394,4 +401,40 @@ constructor(private val constructorApi: ConstructorApi, @ConstructorSdk private var url = "${preferencesHelper.scheme}://${preferencesHelper.quizzesServiceUrl}/${ApiPaths.URL_QUIZ_RESULTS.format(quizId)}${getAdditionalParamsQueryString(encodedParams)}" return constructorApi.getQuizResultsCRT(url) } + + fun trackProductInsightsAgentFocus(body: ProductInsightsAgentItemRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentFocus(body, params.toMap()) + } + + fun trackProductInsightsAgentOutOfView(body: ProductInsightsAgentItemRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentOutOfView(body, params.toMap()) + } + + fun trackProductInsightsAgentView(body: ProductInsightsAgentViewRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentView(body, params.toMap()) + } + + fun trackProductInsightsAgentViews(body: ProductInsightsAgentViewsRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentViews(body, params.toMap()) + } + + fun trackProductInsightsAgentQuestionClick(body: ProductInsightsAgentQuestionRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentQuestionClick(body, params.toMap()) + } + + fun trackProductInsightsAgentQuestionSubmit(body: ProductInsightsAgentQuestionRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentQuestionSubmit(body, params.toMap()) + } + + fun trackProductInsightsAgentAnswerFeedback(body: ProductInsightsAgentAnswerFeedbackRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentAnswerFeedback(body, params.toMap()) + } + + fun trackProductInsightsAgentResultClick(body: ProductInsightsAgentResultClickRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentResultClick(body, params.toMap()) + } + + fun trackProductInsightsAgentAnswerView(body: ProductInsightsAgentAnswerViewRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackProductInsightsAgentAnswerView(body, params.toMap()) + } } diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentAnswerFeedbackTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentAnswerFeedbackTrackingData.kt new file mode 100644 index 00000000..07a921e1 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentAnswerFeedbackTrackingData.kt @@ -0,0 +1,85 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentFeedbackLabel + +/** + * Create a Product Insights Agent answer feedback tracking request object utilizing a builder. Sent when the user gives feedback on an answer. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property feedbackLabel the feedback the user gave on the answer + * @property qnaResultId the result ID of the answer, as returned by the Product Insights Agent + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentAnswerFeedbackTrackingData( + override val itemId: String, + val feedbackLabel: ProductInsightsAgentFeedbackLabel, + val qnaResultId: String? = null, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.feedbackLabel, + builder.qnaResultId, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentAnswerFeedbackTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + feedbackLabel: ProductInsightsAgentFeedbackLabel, + block: Builder.() -> Unit = {} + ) = Builder(itemId, feedbackLabel).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentAnswerFeedbackTrackingData] + */ + class Builder( + val itemId: String, + val feedbackLabel: ProductInsightsAgentFeedbackLabel + ) { + /** The result ID of the answer, as returned by the Product Insights Agent */ + var qnaResultId: String? = null + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [qnaResultId] */ + fun setQnaResultId(qnaResultId: String): Builder = apply { this.qnaResultId = qnaResultId } + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentAnswerFeedbackTrackingData] */ + fun build(): ProductInsightsAgentAnswerFeedbackTrackingData = ProductInsightsAgentAnswerFeedbackTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentAnswerViewTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentAnswerViewTrackingData.kt new file mode 100644 index 00000000..6eaedccc --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentAnswerViewTrackingData.kt @@ -0,0 +1,111 @@ +package io.constructor.data.builder + +import io.constructor.data.model.common.TrackingItem + +/** + * Create a Product Insights Agent answer view tracking request object utilizing a builder. Sent when an answer is shown to the user. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property question the question text, i.e. "Is it waterproof?" + * @property answerText the answer text shown to the user. May be empty. Text over 2000 Unicode code points is truncated to fit + * @property questionTopic the topic the question belongs to, i.e. "Durability" + * @property qnaResultId the result ID of the answer, as returned by the Product Insights Agent + * @property items the items (products) shown in the answer. Only the first 100 are sent + * @property followUpQuestions the follow-up questions shown with the answer. Only the first 100 are sent + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentAnswerViewTrackingData( + override val itemId: String, + val question: String, + val answerText: String, + val questionTopic: String? = null, + val qnaResultId: String? = null, + val items: List? = null, + val followUpQuestions: List? = null, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.question, + builder.answerText, + builder.questionTopic, + builder.qnaResultId, + builder.items, + builder.followUpQuestions, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentAnswerViewTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + question: String, + answerText: String, + block: Builder.() -> Unit = {} + ) = Builder(itemId, question, answerText).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentAnswerViewTrackingData] + */ + class Builder( + val itemId: String, + val question: String, + val answerText: String + ) { + /** The topic the question belongs to, i.e. "Durability" */ + var questionTopic: String? = null + /** The result ID of the answer, as returned by the Product Insights Agent */ + var qnaResultId: String? = null + /** The items (products) shown in the answer. Only the first 100 are sent */ + var items: List? = null + /** The follow-up questions shown with the answer. Only the first 100 are sent */ + var followUpQuestions: List? = null + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [questionTopic] */ + fun setQuestionTopic(questionTopic: String): Builder = apply { this.questionTopic = questionTopic } + /** Sets [qnaResultId] */ + fun setQnaResultId(qnaResultId: String): Builder = apply { this.qnaResultId = qnaResultId } + /** Sets [items] */ + fun setItems(items: List): Builder = apply { this.items = items } + /** Sets [followUpQuestions] */ + fun setFollowUpQuestions(followUpQuestions: List): Builder = apply { this.followUpQuestions = followUpQuestions } + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentAnswerViewTrackingData] */ + fun build(): ProductInsightsAgentAnswerViewTrackingData = ProductInsightsAgentAnswerViewTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentFocusTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentFocusTrackingData.kt new file mode 100644 index 00000000..0ae6a70c --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentFocusTrackingData.kt @@ -0,0 +1,71 @@ +package io.constructor.data.builder + +/** + * Create a Product Insights Agent focus tracking request object utilizing a builder. Sent when the user focuses on (i.e. taps into) the Product Insights Agent. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentFocusTrackingData( + override val itemId: String, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentFocusTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + block: Builder.() -> Unit = {} + ) = Builder(itemId).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentFocusTrackingData] + */ + class Builder( + val itemId: String + ) { + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentFocusTrackingData] */ + fun build(): ProductInsightsAgentFocusTrackingData = ProductInsightsAgentFocusTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentOutOfViewTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentOutOfViewTrackingData.kt new file mode 100644 index 00000000..6380c386 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentOutOfViewTrackingData.kt @@ -0,0 +1,71 @@ +package io.constructor.data.builder + +/** + * Create a Product Insights Agent out of view tracking request object utilizing a builder. Sent when the Product Insights Agent scrolls out of view. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentOutOfViewTrackingData( + override val itemId: String, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentOutOfViewTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + block: Builder.() -> Unit = {} + ) = Builder(itemId).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentOutOfViewTrackingData] + */ + class Builder( + val itemId: String + ) { + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentOutOfViewTrackingData] */ + fun build(): ProductInsightsAgentOutOfViewTrackingData = ProductInsightsAgentOutOfViewTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentQuestionClickTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentQuestionClickTrackingData.kt new file mode 100644 index 00000000..f875508b --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentQuestionClickTrackingData.kt @@ -0,0 +1,83 @@ +package io.constructor.data.builder + +/** + * Create a Product Insights Agent question click tracking request object utilizing a builder. Sent when the user clicks a suggested question. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property question the question text, i.e. "Is it waterproof?" + * @property questionTopic the topic the question belongs to, i.e. "Durability" + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentQuestionClickTrackingData( + override val itemId: String, + val question: String, + val questionTopic: String? = null, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.question, + builder.questionTopic, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentQuestionClickTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + question: String, + block: Builder.() -> Unit = {} + ) = Builder(itemId, question).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentQuestionClickTrackingData] + */ + class Builder( + val itemId: String, + val question: String + ) { + /** The topic the question belongs to, i.e. "Durability" */ + var questionTopic: String? = null + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [questionTopic] */ + fun setQuestionTopic(questionTopic: String): Builder = apply { this.questionTopic = questionTopic } + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentQuestionClickTrackingData] */ + fun build(): ProductInsightsAgentQuestionClickTrackingData = ProductInsightsAgentQuestionClickTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentQuestionSubmitTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentQuestionSubmitTrackingData.kt new file mode 100644 index 00000000..e3eb4c7f --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentQuestionSubmitTrackingData.kt @@ -0,0 +1,83 @@ +package io.constructor.data.builder + +/** + * Create a Product Insights Agent question submit tracking request object utilizing a builder. Sent when the user submits a question. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property question the question text, i.e. "Is it waterproof?" + * @property questionTopic the topic the question belongs to, i.e. "Durability" + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentQuestionSubmitTrackingData( + override val itemId: String, + val question: String, + val questionTopic: String? = null, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.question, + builder.questionTopic, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentQuestionSubmitTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + question: String, + block: Builder.() -> Unit = {} + ) = Builder(itemId, question).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentQuestionSubmitTrackingData] + */ + class Builder( + val itemId: String, + val question: String + ) { + /** The topic the question belongs to, i.e. "Durability" */ + var questionTopic: String? = null + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [questionTopic] */ + fun setQuestionTopic(questionTopic: String): Builder = apply { this.questionTopic = questionTopic } + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentQuestionSubmitTrackingData] */ + fun build(): ProductInsightsAgentQuestionSubmitTrackingData = ProductInsightsAgentQuestionSubmitTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentResultClickTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentResultClickTrackingData.kt new file mode 100644 index 00000000..e38efde7 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentResultClickTrackingData.kt @@ -0,0 +1,109 @@ +package io.constructor.data.builder + +/** + * Create a Product Insights Agent result click tracking request object utilizing a builder. Sent when the user clicks an item (product) recommended in an answer. + * + * @property itemId the ID of the clicked item (product) recommended in the answer, i.e. "1234567-AB" + * @property question the question text, i.e. "Is it waterproof?" + * @property seedItemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. the product detail page item + * @property position the zero-based position of the clicked item within the answer + * @property seedItemName the name of the item (product) the Product Insights Agent is displayed for + * @property seedVariationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property qnaResultId the result ID of the answer, as returned by the Product Insights Agent + * @property itemName the name of the clicked item (product) recommended in the answer + * @property variationId the variation ID of the clicked item (product) recommended in the answer + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentResultClickTrackingData( + override val itemId: String, + val question: String, + val seedItemId: String, + val position: Int? = null, + val seedItemName: String? = null, + val seedVariationId: String? = null, + val qnaResultId: String? = null, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.question, + builder.seedItemId, + builder.position, + builder.seedItemName, + builder.seedVariationId, + builder.qnaResultId, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentResultClickTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + question: String, + seedItemId: String, + block: Builder.() -> Unit = {} + ) = Builder(itemId, question, seedItemId).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentResultClickTrackingData] + */ + class Builder( + val itemId: String, + val question: String, + val seedItemId: String + ) { + /** The zero-based position of the clicked item within the answer */ + var position: Int? = null + /** The name of the item (product) the Product Insights Agent is displayed for */ + var seedItemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var seedVariationId: String? = null + /** The result ID of the answer, as returned by the Product Insights Agent */ + var qnaResultId: String? = null + /** The name of the clicked item (product) recommended in the answer */ + var itemName: String? = null + /** The variation ID of the clicked item (product) recommended in the answer */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [position] */ + fun setPosition(position: Int): Builder = apply { this.position = position } + /** Sets [seedItemName] */ + fun setSeedItemName(seedItemName: String): Builder = apply { this.seedItemName = seedItemName } + /** Sets [seedVariationId] */ + fun setSeedVariationId(seedVariationId: String): Builder = apply { this.seedVariationId = seedVariationId } + /** Sets [qnaResultId] */ + fun setQnaResultId(qnaResultId: String): Builder = apply { this.qnaResultId = qnaResultId } + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentResultClickTrackingData] */ + fun build(): ProductInsightsAgentResultClickTrackingData = ProductInsightsAgentResultClickTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentTrackingData.kt new file mode 100644 index 00000000..d4293ed7 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentTrackingData.kt @@ -0,0 +1,20 @@ +package io.constructor.data.builder + +/** + * The parameters shared by every Product Insights Agent tracking request object + * + * @property itemId the ID of the item (product) the event is for, i.e. "1234567-AB" + * @property itemName the name of the item (product) the event is for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the event is for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +interface ProductInsightsAgentTrackingData { + val itemId: String + val itemName: String? + val variationId: String? + val threadId: String? + val sectionName: String? + val analyticsTags: Map? +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentViewTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentViewTrackingData.kt new file mode 100644 index 00000000..5ac65918 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentViewTrackingData.kt @@ -0,0 +1,78 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion + +/** + * Create a Product Insights Agent view tracking request object utilizing a builder. Sent when the Product Insights Agent comes into view. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property questions the questions shown to the user + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentViewTrackingData( + override val itemId: String, + val questions: List, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.questions, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentViewTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + questions: List, + block: Builder.() -> Unit = {} + ) = Builder(itemId, questions).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentViewTrackingData] + */ + class Builder( + val itemId: String, + val questions: List + ) { + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentViewTrackingData] */ + fun build(): ProductInsightsAgentViewTrackingData = ProductInsightsAgentViewTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentViewsTrackingData.kt b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentViewsTrackingData.kt new file mode 100644 index 00000000..a9d37dc9 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/ProductInsightsAgentViewsTrackingData.kt @@ -0,0 +1,84 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion +import io.constructor.data.model.tracking.ProductInsightsAgentViewTimespan + +/** + * Create a Product Insights Agent views tracking request object utilizing a builder. Sent when the Product Insights Agent has been viewed, reporting each span of time it was in view. + * + * @property itemId the ID of the item (product) the Product Insights Agent is displayed for, i.e. "1234567-AB" + * @property questions the questions shown to the user + * @property viewTimespans the spans of time during which the Product Insights Agent was in view + * @property itemName the name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" + * @property variationId the variation ID of the item (product) the Product Insights Agent is displayed for + * @property threadId the ID of the Product Insights Agent conversation thread + * @property sectionName the index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] + * @property analyticsTags additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision + */ +class ProductInsightsAgentViewsTrackingData( + override val itemId: String, + val questions: List, + val viewTimespans: List, + override val itemName: String? = null, + override val variationId: String? = null, + override val threadId: String? = null, + override val sectionName: String? = null, + override val analyticsTags: Map? = null, +) : ProductInsightsAgentTrackingData { + private constructor(builder: Builder) : this( + builder.itemId, + builder.questions, + builder.viewTimespans, + builder.itemName, + builder.variationId, + builder.threadId, + builder.sectionName, + builder.analyticsTags, + ) + + companion object { + /** + * Builds a [ProductInsightsAgentViewsTrackingData] from the required parameters, applying [block] to set optional parameters + */ + inline fun build( + itemId: String, + questions: List, + viewTimespans: List, + block: Builder.() -> Unit = {} + ) = Builder(itemId, questions, viewTimespans).apply(block).build() + } + + /** + * Builder for [ProductInsightsAgentViewsTrackingData] + */ + class Builder( + val itemId: String, + val questions: List, + val viewTimespans: List + ) { + /** The name of the item (product) the Product Insights Agent is displayed for, i.e. "Waterproof Jacket" */ + var itemName: String? = null + /** The variation ID of the item (product) the Product Insights Agent is displayed for */ + var variationId: String? = null + /** The ID of the Product Insights Agent conversation thread */ + var threadId: String? = null + /** The index section, i.e. "Products". Defaults to [io.constructor.core.ConstructorIoConfig.defaultItemSection] */ + var sectionName: String? = null + /** Additional analytics tags to pass. Merged with [io.constructor.core.ConstructorIoConfig.defaultAnalyticsTags], with request-level values overriding defaults on key collision */ + var analyticsTags: Map? = null + + /** Sets [itemName] */ + fun setItemName(itemName: String): Builder = apply { this.itemName = itemName } + /** Sets [variationId] */ + fun setVariationId(variationId: String): Builder = apply { this.variationId = variationId } + /** Sets [threadId] */ + fun setThreadId(threadId: String): Builder = apply { this.threadId = threadId } + /** Sets [sectionName] */ + fun setSectionName(sectionName: String): Builder = apply { this.sectionName = sectionName } + /** Sets [analyticsTags] */ + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + + /** Builds the [ProductInsightsAgentViewsTrackingData] */ + fun build(): ProductInsightsAgentViewsTrackingData = ProductInsightsAgentViewsTrackingData(this) + } +} diff --git a/library/src/main/java/io/constructor/data/model/common/TrackingItem.kt b/library/src/main/java/io/constructor/data/model/common/TrackingItem.kt index 23d14b1d..bc6f56c1 100644 --- a/library/src/main/java/io/constructor/data/model/common/TrackingItem.kt +++ b/library/src/main/java/io/constructor/data/model/common/TrackingItem.kt @@ -13,4 +13,5 @@ data class TrackingItem( @Json(name = "variation_id") val variationId: String?, @Json(name = "sl_campaign_id") val slCampaignId: String?, @Json(name = "sl_campaign_owner") val slCampaignOwner: String?, + @Json(name = "item_name") val itemName: String? = null, ) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentAnswerFeedbackRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentAnswerFeedbackRequestBody.kt new file mode 100644 index 00000000..7305a4d1 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentAnswerFeedbackRequestBody.kt @@ -0,0 +1,19 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentAnswerFeedbackRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "feedback_label") val feedbackLabel: ProductInsightsAgentFeedbackLabel, + @Json(name = "qna_result_id") val qnaResultId: String?, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentAnswerViewRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentAnswerViewRequestBody.kt new file mode 100644 index 00000000..a2d58898 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentAnswerViewRequestBody.kt @@ -0,0 +1,24 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import io.constructor.data.model.common.TrackingItem +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentAnswerViewRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "question") val question: String, + @Json(name = "answer_text") val answerText: String, + @Json(name = "question_topic") val questionTopic: String?, + @Json(name = "qna_result_id") val qnaResultId: String?, + @Json(name = "items") val items: List?, + @Json(name = "follow_up_questions") val followUpQuestions: List?, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentFeedbackLabel.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentFeedbackLabel.kt new file mode 100644 index 00000000..0a1ab5fe --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentFeedbackLabel.kt @@ -0,0 +1,16 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json + +/** + * The feedback a user gave on a Product Insights Agent answer, used in Product Insights Agent tracking events + */ +enum class ProductInsightsAgentFeedbackLabel { + /** The user found the answer helpful */ + @Json(name = "thumbs_up") + THUMBS_UP, + + /** The user did not find the answer helpful */ + @Json(name = "thumbs_down") + THUMBS_DOWN, +} diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentFollowUpQuestion.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentFollowUpQuestion.kt new file mode 100644 index 00000000..e28d3981 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentFollowUpQuestion.kt @@ -0,0 +1,13 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentFollowUpQuestion( + @Json(name = "value") val value: String, +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentItemRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentItemRequestBody.kt new file mode 100644 index 00000000..9f398d03 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentItemRequestBody.kt @@ -0,0 +1,18 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + * Shared by the Product Insights Agent focus and out of view events, which send the same fields + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentItemRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentQuestion.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentQuestion.kt new file mode 100644 index 00000000..1789b190 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentQuestion.kt @@ -0,0 +1,17 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Models a question shown by the Product Insights Agent, used in Product Insights Agent tracking events + * + * @property question the question text, i.e. "Is it waterproof?" + * @property questionTopic the topic the question belongs to, i.e. "Durability" + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentQuestion( + @Json(name = "question") val question: String, + @Json(name = "question_topic") val questionTopic: String? = null, +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentQuestionRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentQuestionRequestBody.kt new file mode 100644 index 00000000..66091f58 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentQuestionRequestBody.kt @@ -0,0 +1,20 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + * Shared by the Product Insights Agent question click and question submit events, which send the same fields + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentQuestionRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "question") val question: String, + @Json(name = "question_topic") val questionTopic: String?, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentResultClickRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentResultClickRequestBody.kt new file mode 100644 index 00000000..8002e285 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentResultClickRequestBody.kt @@ -0,0 +1,23 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentResultClickRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "question") val question: String, + @Json(name = "seed_item_id") val seedItemId: String, + @Json(name = "position") val position: Int?, + @Json(name = "seed_item_name") val seedItemName: String?, + @Json(name = "seed_variation_id") val seedVariationId: String?, + @Json(name = "qna_result_id") val qnaResultId: String?, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewRequestBody.kt new file mode 100644 index 00000000..f98888f8 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewRequestBody.kt @@ -0,0 +1,18 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentViewRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "questions") val questions: List, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewTimespan.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewTimespan.kt new file mode 100644 index 00000000..a4afce68 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewTimespan.kt @@ -0,0 +1,15 @@ +package io.constructor.data.model.tracking + +import java.io.Serializable + +/** + * Models a span of time during which the Product Insights Agent was in view, used in Product Insights Agent tracking events. + * Sent as ISO-8601 UTC timestamps, i.e. "2026-10-22T02:40:05.123Z" + * + * @property start the time the agent came into view, in epoch milliseconds, i.e. `System.currentTimeMillis()` + * @property end the time the agent went out of view, in epoch milliseconds, i.e. `System.currentTimeMillis()` + */ +data class ProductInsightsAgentViewTimespan( + val start: Long, + val end: Long, +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewTimespanBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewTimespanBody.kt new file mode 100644 index 00000000..7d43ebc7 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewTimespanBody.kt @@ -0,0 +1,14 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentViewTimespanBody( + @Json(name = "start") val start: String, + @Json(name = "end") val end: String, +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewsRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewsRequestBody.kt new file mode 100644 index 00000000..f1c7f37f --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ProductInsightsAgentViewsRequestBody.kt @@ -0,0 +1,19 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * @suppress + */ +@JsonClass(generateAdapter = true) +data class ProductInsightsAgentViewsRequestBody( + @Json(name = "item_id") val itemId: String, + @Json(name = "questions") val questions: List, + @Json(name = "view_timespans") val viewTimespans: List, + @Json(name = "item_name") val itemName: String?, + @Json(name = "variation_id") val variationId: String?, + @Json(name = "thread_id") val threadId: String?, + @Json(name = "analytics_tags") val analyticsTags: Map? +) : Serializable 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 572d145c..5e9fcf41 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -38,4 +38,13 @@ object ApiPaths { const val URL_MEDIA_IMPRESSION_VIEW_EVENT = "v2/ad_behavioral_action/display_ad_view" const val URL_MEDIA_IMPRESSION_CLICK_EVENT = "v2/ad_behavioral_action/display_ad_click" const val URL_RESULTS_IMPRESSION_VIEW_EVENT = "v2/behavioral_action/impression_view" + const val URL_PRODUCT_INSIGHTS_AGENT_FOCUS_EVENT = "v2/behavioral_action/product_insights_agent_focus" + const val URL_PRODUCT_INSIGHTS_AGENT_OUT_OF_VIEW_EVENT = "v2/behavioral_action/product_insights_agent_out_of_view" + const val URL_PRODUCT_INSIGHTS_AGENT_VIEW_EVENT = "v2/behavioral_action/product_insights_agent_view" + const val URL_PRODUCT_INSIGHTS_AGENT_VIEWS_EVENT = "v2/behavioral_action/product_insights_agent_views" + const val URL_PRODUCT_INSIGHTS_AGENT_QUESTION_CLICK_EVENT = "v2/behavioral_action/product_insights_agent_question_click" + const val URL_PRODUCT_INSIGHTS_AGENT_QUESTION_SUBMIT_EVENT = "v2/behavioral_action/product_insights_agent_question_submit" + const val URL_PRODUCT_INSIGHTS_AGENT_ANSWER_FEEDBACK_EVENT = "v2/behavioral_action/product_insights_agent_answer_feedback" + const val URL_PRODUCT_INSIGHTS_AGENT_RESULT_CLICK_EVENT = "v2/behavioral_action/product_insights_agent_result_click" + const val URL_PRODUCT_INSIGHTS_AGENT_ANSWER_VIEW_EVENT = "v2/behavioral_action/product_insights_agent_answer_view" } 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 86873ca8..aa9ff818 100755 --- a/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt +++ b/library/src/main/java/io/constructor/data/remote/ConstructorApi.kt @@ -5,6 +5,13 @@ import io.constructor.data.model.browse.* 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.ProductInsightsAgentAnswerFeedbackRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentAnswerViewRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentItemRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentQuestionRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentResultClickRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewRequestBody +import io.constructor.data.model.tracking.ProductInsightsAgentViewsRequestBody import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody import io.constructor.data.model.conversion.ConversionRequestBody import io.constructor.data.model.purchase.PurchaseRequestBody @@ -159,4 +166,31 @@ interface ConstructorApi { @GET suspend fun getQuizResultsCRT(@Url quizUrl: String): QuizResultsResponse + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_FOCUS_EVENT) + fun trackProductInsightsAgentFocus(@Body body: ProductInsightsAgentItemRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_OUT_OF_VIEW_EVENT) + fun trackProductInsightsAgentOutOfView(@Body body: ProductInsightsAgentItemRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_VIEW_EVENT) + fun trackProductInsightsAgentView(@Body body: ProductInsightsAgentViewRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_VIEWS_EVENT) + fun trackProductInsightsAgentViews(@Body body: ProductInsightsAgentViewsRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_QUESTION_CLICK_EVENT) + fun trackProductInsightsAgentQuestionClick(@Body body: ProductInsightsAgentQuestionRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_QUESTION_SUBMIT_EVENT) + fun trackProductInsightsAgentQuestionSubmit(@Body body: ProductInsightsAgentQuestionRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_ANSWER_FEEDBACK_EVENT) + fun trackProductInsightsAgentAnswerFeedback(@Body body: ProductInsightsAgentAnswerFeedbackRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_RESULT_CLICK_EVENT) + fun trackProductInsightsAgentResultClick(@Body body: ProductInsightsAgentResultClickRequestBody, @QueryMap params: Map): Completable + + @POST(ApiPaths.URL_PRODUCT_INSIGHTS_AGENT_ANSWER_VIEW_EVENT) + fun trackProductInsightsAgentAnswerView(@Body body: ProductInsightsAgentAnswerViewRequestBody, @QueryMap params: Map): Completable } diff --git a/library/src/main/java/io/constructor/util/Extensions.kt b/library/src/main/java/io/constructor/util/Extensions.kt index 66e4f868..40243fb6 100755 --- a/library/src/main/java/io/constructor/util/Extensions.kt +++ b/library/src/main/java/io/constructor/util/Extensions.kt @@ -44,6 +44,14 @@ fun Intent.setExtra(key: String, value: Any) { */ fun String.urlEncode() = URLEncoder.encode(this, "UTF-8").replace("+", "%20") +/** + * Truncates to at most [maxCodePoints] Unicode code points, never splitting a surrogate pair + * + * @suppress + */ +fun String.truncateToCodePoints(maxCodePoints: Int): String = + if (codePointCount(0, length) <= maxCodePoints) this else substring(0, offsetByCodePoints(0, maxCodePoints)) + /** * @suppress */ diff --git a/library/src/test/java/io/constructor/core/ConstructorIoIntegrationTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoIntegrationTest.kt index 8e864163..ca6ed3b9 100644 --- a/library/src/test/java/io/constructor/core/ConstructorIoIntegrationTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoIntegrationTest.kt @@ -6,6 +6,9 @@ import io.constructor.data.local.PreferencesHelper import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.common.VariationsMap import io.constructor.data.model.common.TrackingItem +import io.constructor.data.model.tracking.ProductInsightsAgentFeedbackLabel +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion +import io.constructor.data.model.tracking.ProductInsightsAgentViewTimespan import io.constructor.data.model.purchase.PurchaseItem import io.constructor.test.createTestDataManager import io.constructor.util.RxSchedulersOverrideRule @@ -1867,4 +1870,169 @@ class ConstructorIoIntegrationTest { Thread.sleep(timeBetweenTests) } + + @Test + fun trackProductInsightsAgentFocusAgainstRealResponse() { + val request = ProductInsightsAgentFocusTrackingData.build("10001") { + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentFocusInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + // itemName is optional: the API only needs one of item_id or item_name, and itemId is always required + @Test + fun trackProductInsightsAgentFocusWithoutItemNameAgainstRealResponse() { + val request = ProductInsightsAgentFocusTrackingData.build("10001") + val observer = constructorIo.trackProductInsightsAgentFocusInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentOutOfViewAgainstRealResponse() { + val request = ProductInsightsAgentOutOfViewTrackingData.build("10001") { + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentOutOfViewInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentViewAgainstRealResponse() { + val request = ProductInsightsAgentViewTrackingData.build( + "10001", + listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability"), ProductInsightsAgentQuestion("Is it machine washable?")) + ) { + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentViewInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentViewsAgainstRealResponse() { + val now = System.currentTimeMillis() + val request = ProductInsightsAgentViewsTrackingData.build( + "10001", + listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability")), + listOf(ProductInsightsAgentViewTimespan(now - 10000, now - 5000), ProductInsightsAgentViewTimespan(now - 2000, now)) + ) { + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentViewsInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentQuestionClickAgainstRealResponse() { + val request = ProductInsightsAgentQuestionClickTrackingData.build("10001", "Is it waterproof?") { + questionTopic = "Durability" + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentQuestionClickInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentQuestionSubmitAgainstRealResponse() { + val request = ProductInsightsAgentQuestionSubmitTrackingData.build("10001", "Is it waterproof?") { + questionTopic = "Durability" + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentQuestionSubmitInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentAnswerFeedbackAgainstRealResponse() { + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("10001", ProductInsightsAgentFeedbackLabel.THUMBS_UP) { + qnaResultId = "qna-cdx-709" + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentAnswerFeedbackInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentResultClickAgainstRealResponse() { + val request = ProductInsightsAgentResultClickTrackingData.build("10002", "Is it waterproof?", "10001") { + position = 0 + seedItemName = "item1" + seedVariationId = "20001" + qnaResultId = "qna-cdx-709" + itemName = "item2" + variationId = "20002" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentResultClickInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + @Test + fun trackProductInsightsAgentAnswerViewAgainstRealResponse() { + val request = ProductInsightsAgentAnswerViewTrackingData.build("10001", "Is it waterproof?", "") { + questionTopic = "Durability" + qnaResultId = "qna-cdx-709" + items = listOf(TrackingItem("10002", "20002", testCampaignId, testCampaignOwner, "item2"), TrackingItem("10003", null, null, null)) + followUpQuestions = listOf("Is it breathable?") + itemName = "item1" + variationId = "20001" + threadId = "thread-cdx-709" + analyticsTags = mapOf("test" to "test1") + } + val observer = constructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } + + // Required fields only, with answerText over the API's 2000 code point limit so the truncated text is sent + @Test + fun trackProductInsightsAgentAnswerViewWithRequiredFieldsOnlyAgainstRealResponse() { + val request = ProductInsightsAgentAnswerViewTrackingData.build("10001", "Is it waterproof?", "a".repeat(2001)) + val observer = constructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + observer.assertNoErrors() + Thread.sleep(timeBetweenTests) + } } diff --git a/library/src/test/java/io/constructor/core/ConstructorIoProductInsightsAgentTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoProductInsightsAgentTrackingTest.kt new file mode 100644 index 00000000..1da2d773 --- /dev/null +++ b/library/src/test/java/io/constructor/core/ConstructorIoProductInsightsAgentTrackingTest.kt @@ -0,0 +1,873 @@ +package io.constructor.core + +import android.content.Context +import io.constructor.data.builder.* +import io.constructor.data.local.PreferencesHelper +import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.common.TrackingItem +import io.constructor.data.model.tracking.ProductInsightsAgentFeedbackLabel +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion +import io.constructor.data.model.tracking.ProductInsightsAgentViewTimespan +import io.constructor.test.createTestDataManager +import io.constructor.util.RxSchedulersOverrideRule +import io.mockk.every +import io.mockk.mockk +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.net.SocketTimeoutException +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ConstructorIoProductInsightsAgentTrackingTest { + + @Rule + @JvmField + val overrideSchedulersRule = RxSchedulersOverrideRule() + + private lateinit var mockServer: MockWebServer + private var constructorIo = ConstructorIo + private val ctx = mockk() + private val preferencesHelper = mockk() + private val configMemoryHolder = mockk() + + private val tailFields = listOf("item_name", "variation_id", "thread_id") + + @Before + fun setup() { + mockServer = MockWebServer() + mockServer.start() + + every { ctx.applicationContext } returns ctx + + every { preferencesHelper.apiKey } returns "copper-key" + every { preferencesHelper.id } returns "wacko-the-guid" + every { preferencesHelper.serviceUrl } returns mockServer.hostName + every { preferencesHelper.port } returns mockServer.port + every { preferencesHelper.scheme } returns "http" + every { preferencesHelper.defaultItemSection } returns "Products" + every { preferencesHelper.getSessionId(any(), any()) } returns 67 + every { preferencesHelper.getSessionId() } returns 67 + + every { configMemoryHolder.autocompleteResultCount } returns null + every { configMemoryHolder.defaultAnalyticsTags } returns mapOf("appVersion" to "123", "appPlatform" to "Android") + every { configMemoryHolder.userId } returns "player-three" + every { configMemoryHolder.testCellParams } returns emptyList() + every { configMemoryHolder.segments } returns emptyList() + + val config = ConstructorIoConfig("dummyKey") + val dataManager = createTestDataManager(preferencesHelper, configMemoryHolder) + + constructorIo.testInit(ctx, config, dataManager, preferencesHelper, configMemoryHolder) + } + + @After + fun tearDown() { + mockServer.shutdown() + } + + // region Focus + + @Test + fun trackProductInsightsAgentFocus() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentFocusTrackingData.build("item-1") { + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentFocusInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "focus", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentFocusWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentFocusTrackingData.build("item-1") + val observer = ConstructorIo.trackProductInsightsAgentFocusInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertAbsent(body, tailFields) + } + + @Test + fun trackProductInsightsAgentFocusWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentFocusTrackingData.build("item-1") { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentFocusInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "focus", "Search Suggestions") + val body = getJsonBody(recordedRequest) + assertAbsent(body, listOf("section")) + assertMergedAnalyticsTags(body) + } + + @Test + fun trackProductInsightsAgentFocusUsingPublicMethod() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + ConstructorIo.trackProductInsightsAgentFocus(ProductInsightsAgentFocusTrackingData.build("item-1")) + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "focus", "Products") + assertEquals("item-1", getJsonBody(recordedRequest).getString("item_id")) + } + + @Test + fun trackProductInsightsAgentFocus500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val observer = ConstructorIo.trackProductInsightsAgentFocusInternal(ProductInsightsAgentFocusTrackingData.build("item-1")).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "focus", "Products") + } + + @Test + fun trackProductInsightsAgentFocusTimeout() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error").setBodyDelay(5, TimeUnit.SECONDS)) + val observer = ConstructorIo.trackProductInsightsAgentFocusInternal(ProductInsightsAgentFocusTrackingData.build("item-1")).test() + observer.awaitDone(6, TimeUnit.SECONDS) + observer.assertError(SocketTimeoutException::class.java) + assertPathAndSection(awaitRequest(), "focus", "Products") + } + + // endregion + + // region OutOfView + + @Test + fun trackProductInsightsAgentOutOfView() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentOutOfViewTrackingData.build("item-1") { + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentOutOfViewInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "out_of_view", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentOutOfViewWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val observer = ConstructorIo.trackProductInsightsAgentOutOfViewInternal(ProductInsightsAgentOutOfViewTrackingData.build("item-1")).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertAbsent(body, tailFields) + } + + @Test + fun trackProductInsightsAgentOutOfViewWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentOutOfViewTrackingData.build("item-1") { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentOutOfViewInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "out_of_view", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentOutOfView500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val observer = ConstructorIo.trackProductInsightsAgentOutOfViewInternal(ProductInsightsAgentOutOfViewTrackingData.build("item-1")).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "out_of_view", "Products") + } + + // endregion + + // region View + + @Test + fun trackProductInsightsAgentView() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val questions = listOf( + ProductInsightsAgentQuestion("Is it waterproof?", "Durability"), + ProductInsightsAgentQuestion("Is it machine washable?"), + ) + val request = ProductInsightsAgentViewTrackingData.build("item-1", questions) { + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentViewInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "view", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + val sentQuestions = body.getJSONArray("questions") + assertEquals(2, sentQuestions.length()) + assertEquals("Is it waterproof?", sentQuestions.getJSONObject(0).getString("question")) + assertEquals("Durability", sentQuestions.getJSONObject(0).getString("question_topic")) + assertEquals("Is it machine washable?", sentQuestions.getJSONObject(1).getString("question")) + assertFalse(sentQuestions.getJSONObject(1).has("question_topic")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentViewWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentViewTrackingData.build("item-1", listOf(ProductInsightsAgentQuestion("Is it waterproof?"))) + val observer = ConstructorIo.trackProductInsightsAgentViewInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertEquals(1, body.getJSONArray("questions").length()) + assertAbsent(body, tailFields) + } + + @Test + fun trackProductInsightsAgentViewWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentViewTrackingData.build("item-1", listOf(ProductInsightsAgentQuestion("Is it waterproof?"))) { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentViewInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "view", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentView500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentViewTrackingData.build("item-1", listOf(ProductInsightsAgentQuestion("Is it waterproof?"))) + val observer = ConstructorIo.trackProductInsightsAgentViewInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "view", "Products") + } + + // endregion + + // region Views + + @Test + fun trackProductInsightsAgentViews() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentViewsTrackingData.build( + "item-1", + listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability")), + listOf( + ProductInsightsAgentViewTimespan(1792550405123, 1792550410456), + ProductInsightsAgentViewTimespan(1792550420000, 1792550425000), + ) + ) { + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentViewsInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "views", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + val sentQuestions = body.getJSONArray("questions") + assertEquals("Is it waterproof?", sentQuestions.getJSONObject(0).getString("question")) + assertEquals("Durability", sentQuestions.getJSONObject(0).getString("question_topic")) + // Timespans are given as epoch milliseconds and sent as ISO-8601 UTC timestamps + val timespans = body.getJSONArray("view_timespans") + assertEquals(2, timespans.length()) + assertEquals("2026-10-21T02:40:05.123Z", timespans.getJSONObject(0).getString("start")) + assertEquals("2026-10-21T02:40:10.456Z", timespans.getJSONObject(0).getString("end")) + assertEquals("2026-10-21T02:40:20.000Z", timespans.getJSONObject(1).getString("start")) + assertEquals("2026-10-21T02:40:25.000Z", timespans.getJSONObject(1).getString("end")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentViewsWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentViewsTrackingData.build( + "item-1", + listOf(ProductInsightsAgentQuestion("Is it waterproof?")), + listOf(ProductInsightsAgentViewTimespan(1792550405123, 1792550410456)) + ) + val observer = ConstructorIo.trackProductInsightsAgentViewsInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertEquals(1, body.getJSONArray("view_timespans").length()) + assertAbsent(body, tailFields) + } + + @Test + fun trackProductInsightsAgentViewsSendsUtcTimespansRegardlessOfDefaultTimeZone() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val defaultTimeZone = java.util.TimeZone.getDefault() + try { + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("America/Santiago")) + val request = ProductInsightsAgentViewsTrackingData.build( + "item-1", + listOf(ProductInsightsAgentQuestion("Is it waterproof?")), + listOf(ProductInsightsAgentViewTimespan(1792550405123, 1792550410456)) + ) + val observer = ConstructorIo.trackProductInsightsAgentViewsInternal(request).test() + observer.assertComplete() + val timespan = getJsonBody(awaitRequest()).getJSONArray("view_timespans").getJSONObject(0) + assertEquals("2026-10-21T02:40:05.123Z", timespan.getString("start")) + assertEquals("2026-10-21T02:40:10.456Z", timespan.getString("end")) + } finally { + java.util.TimeZone.setDefault(defaultTimeZone) + } + } + + @Test + fun trackProductInsightsAgentViewsWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentViewsTrackingData.build( + "item-1", + listOf(ProductInsightsAgentQuestion("Is it waterproof?")), + listOf(ProductInsightsAgentViewTimespan(1792550405123, 1792550410456)) + ) { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentViewsInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "views", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentViews500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentViewsTrackingData.build( + "item-1", + listOf(ProductInsightsAgentQuestion("Is it waterproof?")), + listOf(ProductInsightsAgentViewTimespan(1792550405123, 1792550410456)) + ) + val observer = ConstructorIo.trackProductInsightsAgentViewsInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "views", "Products") + } + + // endregion + + // region QuestionClick + + @Test + fun trackProductInsightsAgentQuestionClick() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentQuestionClickTrackingData.build("item-1", "Is it waterproof?") { + questionTopic = "Durability" + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentQuestionClickInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "question_click", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertEquals("Durability", body.getString("question_topic")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentQuestionClickWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentQuestionClickTrackingData.build("item-1", "Is it waterproof?") + val observer = ConstructorIo.trackProductInsightsAgentQuestionClickInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertAbsent(body, listOf("question_topic") + tailFields) + } + + @Test + fun trackProductInsightsAgentQuestionClickWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentQuestionClickTrackingData.build("item-1", "Is it waterproof?") { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentQuestionClickInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "question_click", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentQuestionClick500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentQuestionClickTrackingData.build("item-1", "Is it waterproof?") + val observer = ConstructorIo.trackProductInsightsAgentQuestionClickInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "question_click", "Products") + } + + // endregion + + // region QuestionSubmit + + @Test + fun trackProductInsightsAgentQuestionSubmit() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentQuestionSubmitTrackingData.build("item-1", "Is it waterproof?") { + questionTopic = "Durability" + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentQuestionSubmitInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "question_submit", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertEquals("Durability", body.getString("question_topic")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentQuestionSubmitWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentQuestionSubmitTrackingData.build("item-1", "Is it waterproof?") + val observer = ConstructorIo.trackProductInsightsAgentQuestionSubmitInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertAbsent(body, listOf("question_topic") + tailFields) + } + + @Test + fun trackProductInsightsAgentQuestionSubmitWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentQuestionSubmitTrackingData.build("item-1", "Is it waterproof?") { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentQuestionSubmitInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "question_submit", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentQuestionSubmit500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentQuestionSubmitTrackingData.build("item-1", "Is it waterproof?") + val observer = ConstructorIo.trackProductInsightsAgentQuestionSubmitInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "question_submit", "Products") + } + + // endregion + + // region AnswerFeedback + + @Test + fun trackProductInsightsAgentAnswerFeedback() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("item-1", ProductInsightsAgentFeedbackLabel.THUMBS_UP) { + qnaResultId = "qna-1" + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentAnswerFeedbackInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "answer_feedback", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("thumbs_up", body.getString("feedback_label")) + assertEquals("qna-1", body.getString("qna_result_id")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentAnswerFeedbackThumbsDown() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("item-1", ProductInsightsAgentFeedbackLabel.THUMBS_DOWN) + val observer = ConstructorIo.trackProductInsightsAgentAnswerFeedbackInternal(request).test() + observer.assertComplete() + assertEquals("thumbs_down", getJsonBody(awaitRequest()).getString("feedback_label")) + } + + @Test + fun trackProductInsightsAgentAnswerFeedbackWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("item-1", ProductInsightsAgentFeedbackLabel.THUMBS_UP) + val observer = ConstructorIo.trackProductInsightsAgentAnswerFeedbackInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertEquals("thumbs_up", body.getString("feedback_label")) + assertAbsent(body, listOf("qna_result_id") + tailFields) + } + + @Test + fun trackProductInsightsAgentAnswerFeedbackWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("item-1", ProductInsightsAgentFeedbackLabel.THUMBS_UP) { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentAnswerFeedbackInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "answer_feedback", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentAnswerFeedback500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build("item-1", ProductInsightsAgentFeedbackLabel.THUMBS_UP) + val observer = ConstructorIo.trackProductInsightsAgentAnswerFeedbackInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "answer_feedback", "Products") + } + + // endregion + + // region ResultClick + + @Test + fun trackProductInsightsAgentResultClick() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentResultClickTrackingData.build("rec-1", "Is it waterproof?", "item-1") { + position = 2 + seedItemName = "Waterproof Jacket" + seedVariationId = "var-1" + qnaResultId = "qna-1" + itemName = "Rain Boots" + variationId = "rec-var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentResultClickInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "result_click", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("rec-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertEquals("item-1", body.getString("seed_item_id")) + assertEquals(2, body.getInt("position")) + assertEquals("Waterproof Jacket", body.getString("seed_item_name")) + assertEquals("var-1", body.getString("seed_variation_id")) + assertEquals("qna-1", body.getString("qna_result_id")) + assertEquals("Rain Boots", body.getString("item_name")) + assertEquals("rec-var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentResultClickWithPositionZero() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentResultClickTrackingData.build("rec-1", "Is it waterproof?", "item-1") { + position = 0 + } + val observer = ConstructorIo.trackProductInsightsAgentResultClickInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertTrue(body.has("position")) + assertEquals(0, body.getInt("position")) + } + + @Test + fun trackProductInsightsAgentResultClickWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentResultClickTrackingData.build("rec-1", "Is it waterproof?", "item-1") + val observer = ConstructorIo.trackProductInsightsAgentResultClickInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("rec-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertEquals("item-1", body.getString("seed_item_id")) + assertAbsent(body, listOf("position", "seed_item_name", "seed_variation_id", "qna_result_id") + tailFields) + } + + @Test + fun trackProductInsightsAgentResultClickWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentResultClickTrackingData.build("rec-1", "Is it waterproof?", "item-1") { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentResultClickInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "result_click", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentResultClick500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentResultClickTrackingData.build("rec-1", "Is it waterproof?", "item-1") + val observer = ConstructorIo.trackProductInsightsAgentResultClickInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "result_click", "Products") + } + + // endregion + + // region AnswerView + + @Test + fun trackProductInsightsAgentAnswerView() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", "Yes, it is fully waterproof.") { + questionTopic = "Durability" + qnaResultId = "qna-1" + items = listOf( + TrackingItem("rec-1", "rec-var-1", "camp-1", "owner-a", "Rain Jacket"), + TrackingItem("rec-2", null, null, null), + ) + followUpQuestions = listOf("Is it breathable?", "Does it come in blue?") + itemName = "Waterproof Jacket" + variationId = "var-1" + threadId = "thread-1" + } + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertEquals("POST", recordedRequest.method) + assertPathAndSection(recordedRequest, "answer_view", "Products") + + val body = getJsonBody(recordedRequest) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertEquals("Yes, it is fully waterproof.", body.getString("answer_text")) + assertEquals("Durability", body.getString("question_topic")) + assertEquals("qna-1", body.getString("qna_result_id")) + assertEquals("Waterproof Jacket", body.getString("item_name")) + assertEquals("var-1", body.getString("variation_id")) + assertEquals("thread-1", body.getString("thread_id")) + + val items = body.getJSONArray("items") + assertEquals(2, items.length()) + val firstItem = items.getJSONObject(0) + assertEquals("rec-1", firstItem.getString("item_id")) + assertEquals("rec-var-1", firstItem.getString("variation_id")) + assertEquals("camp-1", firstItem.getString("sl_campaign_id")) + assertEquals("owner-a", firstItem.getString("sl_campaign_owner")) + assertEquals("Rain Jacket", firstItem.getString("item_name")) + val secondItem = items.getJSONObject(1) + assertEquals("rec-2", secondItem.getString("item_id")) + assertEquals(setOf("item_id"), secondItem.keys().asSequence().toSet()) + + val followUpQuestions = body.getJSONArray("follow_up_questions") + assertEquals(2, followUpQuestions.length()) + assertEquals("Is it breathable?", followUpQuestions.getJSONObject(0).getString("value")) + assertEquals("Does it come in blue?", followUpQuestions.getJSONObject(1).getString("value")) + assertClientParams(recordedRequest, body) + } + + @Test + fun trackProductInsightsAgentAnswerViewWithEmptyAnswerText() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", "") + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertTrue(body.has("answer_text")) + assertEquals("", body.getString("answer_text")) + } + + @Test + fun trackProductInsightsAgentAnswerViewCapsListsAt100() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", "Yes.") { + items = (1..150).map { TrackingItem("rec-$it", null, null, null) } + followUpQuestions = (1..150).map { "Question $it?" } + } + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + val items = body.getJSONArray("items") + assertEquals(100, items.length()) + assertEquals("rec-1", items.getJSONObject(0).getString("item_id")) + assertEquals("rec-100", items.getJSONObject(99).getString("item_id")) + val followUpQuestions = body.getJSONArray("follow_up_questions") + assertEquals(100, followUpQuestions.length()) + assertEquals("Question 1?", followUpQuestions.getJSONObject(0).getString("value")) + assertEquals("Question 100?", followUpQuestions.getJSONObject(99).getString("value")) + } + + private fun trackAnswerViewAndGetAnswerText(answerText: String): String { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", answerText) + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + return getJsonBody(awaitRequest()).getString("answer_text") + } + + @Test + fun trackProductInsightsAgentAnswerViewKeepsAnswerTextAt2000CodePoints() { + val answerText = "a".repeat(2000) + assertEquals(answerText, trackAnswerViewAndGetAnswerText(answerText)) + } + + @Test + fun trackProductInsightsAgentAnswerViewTruncatesAnswerTextTo2000CodePoints() { + assertEquals("a".repeat(2000), trackAnswerViewAndGetAnswerText("a".repeat(2500))) + } + + @Test + fun trackProductInsightsAgentAnswerViewCountsAnswerTextInCodePointsNotUtf16Units() { + // U+1F600 is 2 UTF-16 units but 1 code point, so 2000 of them are under the limit + val answerText = "😀".repeat(2000) + assertEquals(answerText, trackAnswerViewAndGetAnswerText(answerText)) + } + + @Test + fun trackProductInsightsAgentAnswerViewTruncatesAnswerTextWithoutSplittingSurrogatePairs() { + // 2001 code points: the cut falls between emoji, so no half surrogate pair is sent + assertEquals("a" + "\uD83D\uDE00".repeat(1999), trackAnswerViewAndGetAnswerText("a" + "\uD83D\uDE00".repeat(2000))) + } + + @Test + fun trackProductInsightsAgentAnswerViewWithRequiredFieldsOnly() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", "Yes.") + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + val body = getJsonBody(awaitRequest()) + assertEquals("item-1", body.getString("item_id")) + assertEquals("Is it waterproof?", body.getString("question")) + assertEquals("Yes.", body.getString("answer_text")) + assertAbsent(body, listOf("question_topic", "qna_result_id", "items", "follow_up_questions") + tailFields) + } + + @Test + fun trackProductInsightsAgentAnswerViewWithSectionAndAnalyticsTags() { + mockServer.enqueue(MockResponse().setResponseCode(204)) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", "Yes.") { + sectionName = "Search Suggestions" + analyticsTags = mapOf("test" to "test1", "appVersion" to "150") + } + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertComplete() + val recordedRequest = awaitRequest() + assertPathAndSection(recordedRequest, "answer_view", "Search Suggestions") + assertMergedAnalyticsTags(getJsonBody(recordedRequest)) + } + + @Test + fun trackProductInsightsAgentAnswerView500() { + mockServer.enqueue(MockResponse().setResponseCode(500).setBody("Internal server error")) + val request = ProductInsightsAgentAnswerViewTrackingData.build("item-1", "Is it waterproof?", "Yes.") + val observer = ConstructorIo.trackProductInsightsAgentAnswerViewInternal(request).test() + observer.assertError { true } + assertPathAndSection(awaitRequest(), "answer_view", "Products") + } + + // endregion + + private fun awaitRequest(): RecordedRequest { + return mockServer.takeRequest(1, TimeUnit.SECONDS) + ?: throw AssertionError("Expected request but timed out") + } + + private fun getJsonBody(request: RecordedRequest): JSONObject = JSONObject(request.body.readUtf8()) + + private fun assertPathAndSection(request: RecordedRequest, suffix: String, section: String) { + val url = request.requestUrl!! + assertEquals("/v2/behavioral_action/product_insights_agent_$suffix", url.encodedPath) + assertEquals(section, url.queryParameter("section")) + } + + // The client parameters are added to the query by RequestInterceptor, not duplicated in the body + private fun assertClientParams(request: RecordedRequest, body: JSONObject) { + val url = request.requestUrl!! + assertEquals("wacko-the-guid", url.queryParameter("i")) + assertEquals("67", url.queryParameter("s")) + assertEquals("copper-key", url.queryParameter("key")) + assertEquals("player-three", url.queryParameter("ui")) + assertTrue(url.queryParameter("c")!!.startsWith("cioand-")) + assertTrue(url.queryParameter("_dt")!!.toLong() > 0) + assertAbsent(body, listOf("c", "i", "s", "key", "ui", "us", "_dt", "beacon", "section")) + val analyticsTags = body.getJSONObject("analytics_tags") + assertEquals("123", analyticsTags.getString("appVersion")) + assertEquals("Android", analyticsTags.getString("appPlatform")) + } + + private fun assertMergedAnalyticsTags(body: JSONObject) { + val analyticsTags = body.getJSONObject("analytics_tags") + assertEquals(3, analyticsTags.length()) + assertEquals("150", analyticsTags.getString("appVersion")) + assertEquals("Android", analyticsTags.getString("appPlatform")) + assertEquals("test1", analyticsTags.getString("test")) + } + + private fun assertAbsent(body: JSONObject, keys: List) { + keys.forEach { assertFalse(body.has(it), "Expected \"$it\" to be absent from the request body") } + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentAnswerFeedbackTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentAnswerFeedbackTrackingDataTest.kt new file mode 100644 index 00000000..036dd5c6 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentAnswerFeedbackTrackingDataTest.kt @@ -0,0 +1,92 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentFeedbackLabel +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentAnswerFeedbackTrackingDataTest { + private val itemId = "item-1" + private val feedbackLabel = ProductInsightsAgentFeedbackLabel.THUMBS_UP + private val qnaResultId = "qna-1" + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun answerFeedbackTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build(itemId, feedbackLabel) + assertEquals(itemId, request.itemId) + assertEquals(feedbackLabel, request.feedbackLabel) + assertNull(request.qnaResultId) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun answerFeedbackTrackingDataUsingDsl() { + val request = ProductInsightsAgentAnswerFeedbackTrackingData.build(itemId, feedbackLabel) { + this.qnaResultId = this@ProductInsightsAgentAnswerFeedbackTrackingDataTest.qnaResultId + this.itemName = this@ProductInsightsAgentAnswerFeedbackTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentAnswerFeedbackTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentAnswerFeedbackTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentAnswerFeedbackTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentAnswerFeedbackTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(feedbackLabel, request.feedbackLabel) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun answerFeedbackTrackingDataUsingBuilder() { + val request = ProductInsightsAgentAnswerFeedbackTrackingData.Builder(itemId, feedbackLabel) + .setQnaResultId(qnaResultId) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(feedbackLabel, request.feedbackLabel) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun answerFeedbackTrackingDataUsingConstructor() { + val request = ProductInsightsAgentAnswerFeedbackTrackingData( + itemId = itemId, + feedbackLabel = feedbackLabel, + qnaResultId = qnaResultId, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(feedbackLabel, request.feedbackLabel) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentAnswerViewTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentAnswerViewTrackingDataTest.kt new file mode 100644 index 00000000..1bfa412d --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentAnswerViewTrackingDataTest.kt @@ -0,0 +1,128 @@ +package io.constructor.data.builder + +import io.constructor.data.model.common.TrackingItem +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentAnswerViewTrackingDataTest { + private val itemId = "item-1" + private val question = "Is it waterproof?" + private val answerText = "Yes, it is fully waterproof." + private val questionTopic = "Durability" + private val qnaResultId = "qna-1" + private val items = listOf(TrackingItem("rec-1", null, null, null)) + private val followUpQuestions = listOf("Is it breathable?") + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun answerViewTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentAnswerViewTrackingData.build(itemId, question, answerText) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(answerText, request.answerText) + assertNull(request.questionTopic) + assertNull(request.qnaResultId) + assertNull(request.items) + assertNull(request.followUpQuestions) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun answerViewTrackingDataUsingDsl() { + val request = ProductInsightsAgentAnswerViewTrackingData.build(itemId, question, answerText) { + this.questionTopic = this@ProductInsightsAgentAnswerViewTrackingDataTest.questionTopic + this.qnaResultId = this@ProductInsightsAgentAnswerViewTrackingDataTest.qnaResultId + this.items = this@ProductInsightsAgentAnswerViewTrackingDataTest.items + this.followUpQuestions = this@ProductInsightsAgentAnswerViewTrackingDataTest.followUpQuestions + this.itemName = this@ProductInsightsAgentAnswerViewTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentAnswerViewTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentAnswerViewTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentAnswerViewTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentAnswerViewTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(answerText, request.answerText) + assertEquals(questionTopic, request.questionTopic) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(items, request.items) + assertEquals(followUpQuestions, request.followUpQuestions) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun answerViewTrackingDataUsingBuilder() { + val request = ProductInsightsAgentAnswerViewTrackingData.Builder(itemId, question, answerText) + .setQuestionTopic(questionTopic) + .setQnaResultId(qnaResultId) + .setItems(items) + .setFollowUpQuestions(followUpQuestions) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(answerText, request.answerText) + assertEquals(questionTopic, request.questionTopic) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(items, request.items) + assertEquals(followUpQuestions, request.followUpQuestions) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun answerViewTrackingDataUsingConstructor() { + val request = ProductInsightsAgentAnswerViewTrackingData( + itemId = itemId, + question = question, + answerText = answerText, + questionTopic = questionTopic, + qnaResultId = qnaResultId, + items = items, + followUpQuestions = followUpQuestions, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(answerText, request.answerText) + assertEquals(questionTopic, request.questionTopic) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(items, request.items) + assertEquals(followUpQuestions, request.followUpQuestions) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun answerViewTrackingDataKeepsEmptyAnswerText() { + val request = ProductInsightsAgentAnswerViewTrackingData.build(itemId, question, "") + assertEquals("", request.answerText) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentFocusTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentFocusTrackingDataTest.kt new file mode 100644 index 00000000..61a780a5 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentFocusTrackingDataTest.kt @@ -0,0 +1,77 @@ +package io.constructor.data.builder + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentFocusTrackingDataTest { + private val itemId = "item-1" + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun focusTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentFocusTrackingData.build(itemId) + assertEquals(itemId, request.itemId) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun focusTrackingDataUsingDsl() { + val request = ProductInsightsAgentFocusTrackingData.build(itemId) { + this.itemName = this@ProductInsightsAgentFocusTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentFocusTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentFocusTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentFocusTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentFocusTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun focusTrackingDataUsingBuilder() { + val request = ProductInsightsAgentFocusTrackingData.Builder(itemId) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun focusTrackingDataUsingConstructor() { + val request = ProductInsightsAgentFocusTrackingData( + itemId = itemId, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentOutOfViewTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentOutOfViewTrackingDataTest.kt new file mode 100644 index 00000000..610b4699 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentOutOfViewTrackingDataTest.kt @@ -0,0 +1,77 @@ +package io.constructor.data.builder + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentOutOfViewTrackingDataTest { + private val itemId = "item-1" + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun outOfViewTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentOutOfViewTrackingData.build(itemId) + assertEquals(itemId, request.itemId) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun outOfViewTrackingDataUsingDsl() { + val request = ProductInsightsAgentOutOfViewTrackingData.build(itemId) { + this.itemName = this@ProductInsightsAgentOutOfViewTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentOutOfViewTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentOutOfViewTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentOutOfViewTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentOutOfViewTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun outOfViewTrackingDataUsingBuilder() { + val request = ProductInsightsAgentOutOfViewTrackingData.Builder(itemId) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun outOfViewTrackingDataUsingConstructor() { + val request = ProductInsightsAgentOutOfViewTrackingData( + itemId = itemId, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentQuestionClickTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentQuestionClickTrackingDataTest.kt new file mode 100644 index 00000000..4abc492e --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentQuestionClickTrackingDataTest.kt @@ -0,0 +1,91 @@ +package io.constructor.data.builder + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentQuestionClickTrackingDataTest { + private val itemId = "item-1" + private val question = "Is it waterproof?" + private val questionTopic = "Durability" + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun questionClickTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentQuestionClickTrackingData.build(itemId, question) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertNull(request.questionTopic) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun questionClickTrackingDataUsingDsl() { + val request = ProductInsightsAgentQuestionClickTrackingData.build(itemId, question) { + this.questionTopic = this@ProductInsightsAgentQuestionClickTrackingDataTest.questionTopic + this.itemName = this@ProductInsightsAgentQuestionClickTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentQuestionClickTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentQuestionClickTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentQuestionClickTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentQuestionClickTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(questionTopic, request.questionTopic) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun questionClickTrackingDataUsingBuilder() { + val request = ProductInsightsAgentQuestionClickTrackingData.Builder(itemId, question) + .setQuestionTopic(questionTopic) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(questionTopic, request.questionTopic) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun questionClickTrackingDataUsingConstructor() { + val request = ProductInsightsAgentQuestionClickTrackingData( + itemId = itemId, + question = question, + questionTopic = questionTopic, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(questionTopic, request.questionTopic) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentQuestionSubmitTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentQuestionSubmitTrackingDataTest.kt new file mode 100644 index 00000000..398c4fc2 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentQuestionSubmitTrackingDataTest.kt @@ -0,0 +1,91 @@ +package io.constructor.data.builder + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentQuestionSubmitTrackingDataTest { + private val itemId = "item-1" + private val question = "Is it waterproof?" + private val questionTopic = "Durability" + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun questionSubmitTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentQuestionSubmitTrackingData.build(itemId, question) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertNull(request.questionTopic) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun questionSubmitTrackingDataUsingDsl() { + val request = ProductInsightsAgentQuestionSubmitTrackingData.build(itemId, question) { + this.questionTopic = this@ProductInsightsAgentQuestionSubmitTrackingDataTest.questionTopic + this.itemName = this@ProductInsightsAgentQuestionSubmitTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentQuestionSubmitTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentQuestionSubmitTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentQuestionSubmitTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentQuestionSubmitTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(questionTopic, request.questionTopic) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun questionSubmitTrackingDataUsingBuilder() { + val request = ProductInsightsAgentQuestionSubmitTrackingData.Builder(itemId, question) + .setQuestionTopic(questionTopic) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(questionTopic, request.questionTopic) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun questionSubmitTrackingDataUsingConstructor() { + val request = ProductInsightsAgentQuestionSubmitTrackingData( + itemId = itemId, + question = question, + questionTopic = questionTopic, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(questionTopic, request.questionTopic) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentResultClickTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentResultClickTrackingDataTest.kt new file mode 100644 index 00000000..4f9f4434 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentResultClickTrackingDataTest.kt @@ -0,0 +1,129 @@ +package io.constructor.data.builder + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentResultClickTrackingDataTest { + private val itemId = "item-1" + private val question = "Is it waterproof?" + private val seedItemId = "seed-1" + private val position = 0 + private val seedItemName = "Seed Jacket" + private val seedVariationId = "seed-var-1" + private val qnaResultId = "qna-1" + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun resultClickTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentResultClickTrackingData.build(itemId, question, seedItemId) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(seedItemId, request.seedItemId) + assertNull(request.position) + assertNull(request.seedItemName) + assertNull(request.seedVariationId) + assertNull(request.qnaResultId) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun resultClickTrackingDataUsingDsl() { + val request = ProductInsightsAgentResultClickTrackingData.build(itemId, question, seedItemId) { + this.position = this@ProductInsightsAgentResultClickTrackingDataTest.position + this.seedItemName = this@ProductInsightsAgentResultClickTrackingDataTest.seedItemName + this.seedVariationId = this@ProductInsightsAgentResultClickTrackingDataTest.seedVariationId + this.qnaResultId = this@ProductInsightsAgentResultClickTrackingDataTest.qnaResultId + this.itemName = this@ProductInsightsAgentResultClickTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentResultClickTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentResultClickTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentResultClickTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentResultClickTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(seedItemId, request.seedItemId) + assertEquals(position, request.position) + assertEquals(seedItemName, request.seedItemName) + assertEquals(seedVariationId, request.seedVariationId) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun resultClickTrackingDataUsingBuilder() { + val request = ProductInsightsAgentResultClickTrackingData.Builder(itemId, question, seedItemId) + .setPosition(position) + .setSeedItemName(seedItemName) + .setSeedVariationId(seedVariationId) + .setQnaResultId(qnaResultId) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(seedItemId, request.seedItemId) + assertEquals(position, request.position) + assertEquals(seedItemName, request.seedItemName) + assertEquals(seedVariationId, request.seedVariationId) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun resultClickTrackingDataUsingConstructor() { + val request = ProductInsightsAgentResultClickTrackingData( + itemId = itemId, + question = question, + seedItemId = seedItemId, + position = position, + seedItemName = seedItemName, + seedVariationId = seedVariationId, + qnaResultId = qnaResultId, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(question, request.question) + assertEquals(seedItemId, request.seedItemId) + assertEquals(position, request.position) + assertEquals(seedItemName, request.seedItemName) + assertEquals(seedVariationId, request.seedVariationId) + assertEquals(qnaResultId, request.qnaResultId) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun resultClickTrackingDataKeepsPositionZero() { + val request = ProductInsightsAgentResultClickTrackingData.build(itemId, question, seedItemId) { + position = 0 + } + assertEquals(0, request.position) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentTrackingDataTest.kt new file mode 100644 index 00000000..2cb4d8cd --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentTrackingDataTest.kt @@ -0,0 +1,36 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentFeedbackLabel +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion +import io.constructor.data.model.tracking.ProductInsightsAgentViewTimespan +import org.junit.Test +import kotlin.test.assertEquals + +class ProductInsightsAgentTrackingDataTest { + private val itemId = "item-1" + private val questions = listOf(ProductInsightsAgentQuestion("Is it waterproof?")) + + @Test + fun everyEventExposesTheSharedFieldsThroughTheInterface() { + val requests: List = listOf( + ProductInsightsAgentFocusTrackingData(itemId, "Waterproof Jacket", "var-1", "thread-1", "Search Suggestions", mapOf("test" to "test1")), + ProductInsightsAgentOutOfViewTrackingData(itemId, "Waterproof Jacket", "var-1", "thread-1", "Search Suggestions", mapOf("test" to "test1")), + ProductInsightsAgentViewTrackingData(itemId, questions, "Waterproof Jacket", "var-1", "thread-1", "Search Suggestions", mapOf("test" to "test1")), + ProductInsightsAgentViewsTrackingData(itemId, questions, listOf(ProductInsightsAgentViewTimespan(1, 2)), "Waterproof Jacket", "var-1", "thread-1", "Search Suggestions", mapOf("test" to "test1")), + ProductInsightsAgentQuestionClickTrackingData(itemId, "Is it waterproof?", itemName = "Waterproof Jacket", variationId = "var-1", threadId = "thread-1", sectionName = "Search Suggestions", analyticsTags = mapOf("test" to "test1")), + ProductInsightsAgentQuestionSubmitTrackingData(itemId, "Is it waterproof?", itemName = "Waterproof Jacket", variationId = "var-1", threadId = "thread-1", sectionName = "Search Suggestions", analyticsTags = mapOf("test" to "test1")), + ProductInsightsAgentAnswerFeedbackTrackingData(itemId, ProductInsightsAgentFeedbackLabel.THUMBS_UP, itemName = "Waterproof Jacket", variationId = "var-1", threadId = "thread-1", sectionName = "Search Suggestions", analyticsTags = mapOf("test" to "test1")), + ProductInsightsAgentResultClickTrackingData(itemId, "Is it waterproof?", "seed-1", itemName = "Waterproof Jacket", variationId = "var-1", threadId = "thread-1", sectionName = "Search Suggestions", analyticsTags = mapOf("test" to "test1")), + ProductInsightsAgentAnswerViewTrackingData(itemId, "Is it waterproof?", "Yes", itemName = "Waterproof Jacket", variationId = "var-1", threadId = "thread-1", sectionName = "Search Suggestions", analyticsTags = mapOf("test" to "test1")), + ) + requests.forEach { + val name = it::class.simpleName + assertEquals(itemId, it.itemId, name) + assertEquals("Waterproof Jacket", it.itemName, name) + assertEquals("var-1", it.variationId, name) + assertEquals("thread-1", it.threadId, name) + assertEquals("Search Suggestions", it.sectionName, name) + assertEquals(mapOf("test" to "test1"), it.analyticsTags, name) + } + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentViewTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentViewTrackingDataTest.kt new file mode 100644 index 00000000..e64a17d4 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentViewTrackingDataTest.kt @@ -0,0 +1,84 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentViewTrackingDataTest { + private val itemId = "item-1" + private val questions = listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability")) + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun viewTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentViewTrackingData.build(itemId, questions) + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun viewTrackingDataUsingDsl() { + val request = ProductInsightsAgentViewTrackingData.build(itemId, questions) { + this.itemName = this@ProductInsightsAgentViewTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentViewTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentViewTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentViewTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentViewTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun viewTrackingDataUsingBuilder() { + val request = ProductInsightsAgentViewTrackingData.Builder(itemId, questions) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun viewTrackingDataUsingConstructor() { + val request = ProductInsightsAgentViewTrackingData( + itemId = itemId, + questions = questions, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +} diff --git a/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentViewsTrackingDataTest.kt b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentViewsTrackingDataTest.kt new file mode 100644 index 00000000..b7efc459 --- /dev/null +++ b/library/src/test/java/io/constructor/data/builder/ProductInsightsAgentViewsTrackingDataTest.kt @@ -0,0 +1,91 @@ +package io.constructor.data.builder + +import io.constructor.data.model.tracking.ProductInsightsAgentQuestion +import io.constructor.data.model.tracking.ProductInsightsAgentViewTimespan +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ProductInsightsAgentViewsTrackingDataTest { + private val itemId = "item-1" + private val questions = listOf(ProductInsightsAgentQuestion("Is it waterproof?", "Durability")) + private val viewTimespans = listOf(ProductInsightsAgentViewTimespan(1792550405123, 1792550410456)) + private val itemName = "Waterproof Jacket" + private val variationId = "var-1" + private val threadId = "thread-1" + private val sectionName = "Search Suggestions" + private val analyticsTags = mapOf("test" to "test1") + + @Test + fun viewsTrackingDataWithRequiredParams() { + val request = ProductInsightsAgentViewsTrackingData.build(itemId, questions, viewTimespans) + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(viewTimespans, request.viewTimespans) + assertNull(request.itemName) + assertNull(request.variationId) + assertNull(request.threadId) + assertNull(request.sectionName) + assertNull(request.analyticsTags) + } + + @Test + fun viewsTrackingDataUsingDsl() { + val request = ProductInsightsAgentViewsTrackingData.build(itemId, questions, viewTimespans) { + this.itemName = this@ProductInsightsAgentViewsTrackingDataTest.itemName + this.variationId = this@ProductInsightsAgentViewsTrackingDataTest.variationId + this.threadId = this@ProductInsightsAgentViewsTrackingDataTest.threadId + this.sectionName = this@ProductInsightsAgentViewsTrackingDataTest.sectionName + this.analyticsTags = this@ProductInsightsAgentViewsTrackingDataTest.analyticsTags + } + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(viewTimespans, request.viewTimespans) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun viewsTrackingDataUsingBuilder() { + val request = ProductInsightsAgentViewsTrackingData.Builder(itemId, questions, viewTimespans) + .setItemName(itemName) + .setVariationId(variationId) + .setThreadId(threadId) + .setSectionName(sectionName) + .setAnalyticsTags(analyticsTags) + .build() + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(viewTimespans, request.viewTimespans) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } + + @Test + fun viewsTrackingDataUsingConstructor() { + val request = ProductInsightsAgentViewsTrackingData( + itemId = itemId, + questions = questions, + viewTimespans = viewTimespans, + itemName = itemName, + variationId = variationId, + threadId = threadId, + sectionName = sectionName, + analyticsTags = analyticsTags, + ) + assertEquals(itemId, request.itemId) + assertEquals(questions, request.questions) + assertEquals(viewTimespans, request.viewTimespans) + assertEquals(itemName, request.itemName) + assertEquals(variationId, request.variationId) + assertEquals(threadId, request.threadId) + assertEquals(sectionName, request.sectionName) + assertEquals(analyticsTags, request.analyticsTags) + } +}