From 1a4137f5d44019980807ee99b38f9d79ef47b374 Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Thu, 11 Jun 2026 18:59:38 +0200 Subject: [PATCH 1/3] [REM-3440] introduce trackResultsImpressionView to track viewable impressions --- README.md | 20 +++ .../java/io/constructor/core/ConstructorIo.kt | 33 ++++ .../java/io/constructor/data/DataManager.kt | 5 + .../model/common/ResultsImpressionItem.kt | 14 ++ .../ResultsImpressionViewRequestBody.kt | 23 +++ .../io/constructor/data/remote/ApiPaths.kt | 1 + .../constructor/data/remote/ConstructorApi.kt | 4 + ...ctorIoResultsImpressionViewTrackingTest.kt | 153 ++++++++++++++++++ 8 files changed, 253 insertions(+) create mode 100644 library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt create mode 100644 library/src/main/java/io/constructor/data/model/tracking/ResultsImpressionViewRequestBody.kt create mode 100644 library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt diff --git a/README.md b/README.md index 899dba53..dd2360a5 100755 --- a/README.md +++ b/README.md @@ -649,3 +649,23 @@ ConstructorIo.trackMediaImpressionClick( bannerAdId = "abc123", placementId = "home" ) +``` + +### Results Impression View Events + +```kotlin +// Track sponsored results impression view +val items = listOf( + ResultsImpressionItem( + itemId = "product-789", + itemName = "Premium Sneakers", + variationId = "product-789-white", + slCampaignId = "campaign-1", + slCampaignOwner = "owner-a" + ) +) +ConstructorIo.trackResultsImpressionView( + items = items, + searchTerm = "sneakers" +) +``` diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 29c56193..8fb75081 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -25,6 +25,8 @@ import io.constructor.data.model.search.* import io.constructor.data.model.tracking.GenericResultClickRequestBody import io.constructor.data.model.tracking.ItemDetailLoadRequestBody import io.constructor.data.model.tracking.MediaImpressionRequestBody +import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody +import io.constructor.data.model.common.ResultsImpressionItem import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -1966,6 +1968,16 @@ object ConstructorIo { })) } + /** + * Tracks results impression view events (items that were viewably impressed per MRC guidelines). + */ + fun trackResultsImpressionView(items: List, searchTerm: String? = null, filterName: String? = null, filterValue: String? = null, analyticsTags: Map? = null) { + val completable = trackResultsImpressionViewInternal(items, searchTerm, filterName, filterValue, analyticsTags) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { t -> + e("Results Impression View error: ${t.message}") + })) + } + internal fun trackItemDetailLoadedInternal(itemName: String, customerId: String, variationId: String? = null, sectionName: String? = null, url: String = "Not Available", analyticsTags: Map? = null): Completable { preferenceHelper.getSessionId(sessionIncrementHandler) val section = sectionName ?: preferenceHelper.defaultItemSection @@ -2030,6 +2042,27 @@ object ConstructorIo { return dataManager.trackMediaImpressionClick(preferenceHelper, requestBody) } + internal fun trackResultsImpressionViewInternal(items: List, searchTerm: String? = null, filterName: String? = null, filterValue: String? = null, analyticsTags: Map? = null): Completable { + preferenceHelper.getSessionId(sessionIncrementHandler) + val requestBody = ResultsImpressionViewRequestBody( + items, + searchTerm, + filterName, + filterValue, + mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags), + true, + BuildConfig.CLIENT_VERSION, + preferenceHelper.id, + preferenceHelper.getSessionId(), + preferenceHelper.apiKey, + configMemoryHolder.userId, + configMemoryHolder.segments, + System.currentTimeMillis() + ) + + return dataManager.trackResultsImpressionView(requestBody) + } + /** * Tracks generic result click events. * diff --git a/library/src/main/java/io/constructor/data/DataManager.kt b/library/src/main/java/io/constructor/data/DataManager.kt index 942ea948..07f54dbe 100755 --- a/library/src/main/java/io/constructor/data/DataManager.kt +++ b/library/src/main/java/io/constructor/data/DataManager.kt @@ -14,6 +14,7 @@ import io.constructor.data.model.recommendations.RecommendationsResponse import io.constructor.data.model.search.* import io.constructor.data.model.tracking.GenericResultClickRequestBody import io.constructor.data.model.tracking.MediaImpressionRequestBody +import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody import io.constructor.data.remote.ApiPaths import io.constructor.data.remote.ConstructorApi import io.constructor.injection.ConstructorSdk @@ -314,6 +315,10 @@ constructor(private val constructorApi: ConstructorApi, @ConstructorSdk private return constructorApi.trackQuizConversion(quizConversionRequestBody, params.toMap()) } + fun trackResultsImpressionView(resultsImpressionViewRequestBody: ResultsImpressionViewRequestBody, params: Array> = arrayOf()): Completable { + return constructorApi.trackResultsImpressionView(resultsImpressionViewRequestBody, params.toMap()) + } + fun trackMediaImpressionView(preferencesHelper: PreferencesHelper, mediaImpressionRequestBody: MediaImpressionRequestBody): Completable { val url = buildMediaUrl(preferencesHelper, ApiPaths.URL_MEDIA_IMPRESSION_VIEW_EVENT) return constructorApi.trackMediaImpressionView(url, mediaImpressionRequestBody) diff --git a/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt b/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt new file mode 100644 index 00000000..570691b0 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt @@ -0,0 +1,14 @@ +package io.constructor.data.model.common + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class ResultsImpressionItem( + @Json(name = "item_id") val itemId: String, + @Json(name = "item_name") val itemName: String, + @Json(name = "variation_id") val variationId: String? = null, + @Json(name = "sl_campaign_id") val slCampaignId: String? = null, + @Json(name = "sl_campaign_owner") val slCampaignOwner: String? = null, +) : Serializable diff --git a/library/src/main/java/io/constructor/data/model/tracking/ResultsImpressionViewRequestBody.kt b/library/src/main/java/io/constructor/data/model/tracking/ResultsImpressionViewRequestBody.kt new file mode 100644 index 00000000..fd3f2d87 --- /dev/null +++ b/library/src/main/java/io/constructor/data/model/tracking/ResultsImpressionViewRequestBody.kt @@ -0,0 +1,23 @@ +package io.constructor.data.model.tracking + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import io.constructor.data.model.common.ResultsImpressionItem +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class ResultsImpressionViewRequestBody( + @Json(name = "items") val items: List, + @Json(name = "search_term") val searchTerm: String?, + @Json(name = "filter_name") val filterName: String?, + @Json(name = "filter_value") val filterValue: String?, + @Json(name = "analytics_tags") val analyticsTags: Map?, + @Json(name = "beacon") val beacon: Boolean = true, + @Json(name = "c") val c: String, + @Json(name = "i") val i: String, + @Json(name = "s") val s: Int, + @Json(name = "key") val key: String, + @Json(name = "ui") val ui: String?, + @Json(name = "us") val us: List, + @Json(name = "_dt") val _dt: Long? +) : 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 d757f8bd..572d145c 100755 --- a/library/src/main/java/io/constructor/data/remote/ApiPaths.kt +++ b/library/src/main/java/io/constructor/data/remote/ApiPaths.kt @@ -37,4 +37,5 @@ object ApiPaths { const val URL_QUIZ_RESULTS = "v1/quizzes/%s/results" 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" } 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 e94b8d9f..86873ca8 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,7 @@ 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.ResultsImpressionViewRequestBody import io.constructor.data.model.conversion.ConversionRequestBody import io.constructor.data.model.purchase.PurchaseRequestBody import io.constructor.data.model.quiz.* @@ -144,6 +145,9 @@ interface ConstructorApi { @POST fun trackMediaImpressionClick(@Url url: String, @Body mediaImpressionRequestBody: MediaImpressionRequestBody): Completable + @POST(ApiPaths.URL_RESULTS_IMPRESSION_VIEW_EVENT) + fun trackResultsImpressionView(@Body body: ResultsImpressionViewRequestBody, @QueryMap params: Map): Completable + @GET fun getQuizNextQuestion(@Url quizUrl: String): Single> diff --git a/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt new file mode 100644 index 00000000..338e53a6 --- /dev/null +++ b/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt @@ -0,0 +1,153 @@ +package io.constructor.core + +import android.content.Context +import io.constructor.data.local.PreferencesHelper +import io.constructor.data.memory.ConfigMemoryHolder +import io.constructor.data.model.common.ResultsImpressionItem +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.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.assertTrue + +class ConstructorIoResultsImpressionViewTrackingTest { + + @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() + + @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 { 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() + } + + @Test + fun trackResultsImpressionView() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + + val items = listOf( + ResultsImpressionItem("item-1", "Item One"), + ResultsImpressionItem("item-2", "Item Two", variationId = "var-2") + ) + val observer = ConstructorIo.trackResultsImpressionViewInternal(items).test() + observer.assertComplete() + val request = awaitRequest() + assertEquals("POST", request.method) + assertTrue(request.path!!.startsWith("/v2/behavioral_action/impression_view")) + + val requestBody = getRequestBody(request) + assertEquals("true", requestBody["beacon"]) + assertTrue(requestBody["items"]!!.contains("item_id:item-1")) + assertTrue(requestBody["items"]!!.contains("item_name:Item One")) + assertTrue(requestBody["items"]!!.contains("item_id:item-2")) + assertTrue(requestBody["items"]!!.contains("variation_id:var-2")) + } + + @Test + fun trackResultsImpressionViewWithSearchTerm() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + + val items = listOf(ResultsImpressionItem("item-1", "Item One", slCampaignId = "camp-1", slCampaignOwner = "owner-a")) + val observer = ConstructorIo.trackResultsImpressionViewInternal(items, searchTerm = "shoes").test() + observer.assertComplete() + val request = awaitRequest() + + val requestBody = getRequestBody(request) + assertEquals("shoes", requestBody["search_term"]) + assertTrue(requestBody["items"]!!.contains("sl_campaign_id:camp-1")) + assertTrue(requestBody["items"]!!.contains("sl_campaign_owner:owner-a")) + } + + @Test + fun trackResultsImpressionViewWithFilters() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + + val items = listOf(ResultsImpressionItem("item-1", "Item One")) + val observer = ConstructorIo.trackResultsImpressionViewInternal(items, filterName = "category_id", filterValue = "shoes-123").test() + observer.assertComplete() + val request = awaitRequest() + + val requestBody = getRequestBody(request) + assertEquals("category_id", requestBody["filter_name"]) + assertEquals("shoes-123", requestBody["filter_value"]) + } + + @Test + fun trackResultsImpressionView500() { + val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") + mockServer.enqueue(mockResponse) + + val items = listOf(ResultsImpressionItem("item-1", "Item One")) + val observer = ConstructorIo.trackResultsImpressionViewInternal(items).test() + observer.assertError { true } + val request = awaitRequest() + assertTrue(request.path!!.startsWith("/v2/behavioral_action/impression_view")) + } + + @Test + fun trackResultsImpressionViewTimeout() { + val mockResponse = MockResponse() + .setResponseCode(500) + .setBody("Internal server error") + .setBodyDelay(5, TimeUnit.SECONDS) + mockServer.enqueue(mockResponse) + + val items = listOf(ResultsImpressionItem("item-1", "Item One")) + val observer = ConstructorIo.trackResultsImpressionViewInternal(items).test() + observer.awaitDone(6, TimeUnit.SECONDS) + observer.assertError(SocketTimeoutException::class.java) + val request = awaitRequest() + assertTrue(request.path!!.startsWith("/v2/behavioral_action/impression_view")) + } + + private fun awaitRequest(): RecordedRequest { + return mockServer.takeRequest(1, TimeUnit.SECONDS) + ?: throw AssertionError("Expected request but timed out") + } +} From 4a3f504f7272d1150ee92ea8913c98cf6d10ac91 Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Mon, 15 Jun 2026 14:50:20 +0200 Subject: [PATCH 2/3] [REM-3440] Address test and code style comments --- .../model/common/ResultsImpressionItem.kt | 2 +- ...ctorIoResultsImpressionViewTrackingTest.kt | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt b/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt index 570691b0..e8cc8638 100644 --- a/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt +++ b/library/src/main/java/io/constructor/data/model/common/ResultsImpressionItem.kt @@ -10,5 +10,5 @@ data class ResultsImpressionItem( @Json(name = "item_name") val itemName: String, @Json(name = "variation_id") val variationId: String? = null, @Json(name = "sl_campaign_id") val slCampaignId: String? = null, - @Json(name = "sl_campaign_owner") val slCampaignOwner: String? = null, + @Json(name = "sl_campaign_owner") val slCampaignOwner: String? = null ) : Serializable diff --git a/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt index 338e53a6..4faae7fc 100644 --- a/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoResultsImpressionViewTrackingTest.kt @@ -18,7 +18,6 @@ import org.junit.Test import java.net.SocketTimeoutException import java.util.concurrent.TimeUnit import kotlin.test.assertEquals -import kotlin.test.assertTrue class ConstructorIoResultsImpressionViewTrackingTest { @@ -77,14 +76,16 @@ class ConstructorIoResultsImpressionViewTrackingTest { observer.assertComplete() val request = awaitRequest() assertEquals("POST", request.method) - assertTrue(request.path!!.startsWith("/v2/behavioral_action/impression_view")) + assert(request.path!!.startsWith("/v2/behavioral_action/impression_view")) val requestBody = getRequestBody(request) assertEquals("true", requestBody["beacon"]) - assertTrue(requestBody["items"]!!.contains("item_id:item-1")) - assertTrue(requestBody["items"]!!.contains("item_name:Item One")) - assertTrue(requestBody["items"]!!.contains("item_id:item-2")) - assertTrue(requestBody["items"]!!.contains("variation_id:var-2")) + assertEquals("wacko-the-guid", requestBody["i"]) + assertEquals("67", requestBody["s"]) + assert(requestBody["items"]!!.contains("item_id:item-1")) + assert(requestBody["items"]!!.contains("item_name:Item One")) + assert(requestBody["items"]!!.contains("item_id:item-2")) + assert(requestBody["items"]!!.contains("variation_id:var-2")) } @Test @@ -99,8 +100,8 @@ class ConstructorIoResultsImpressionViewTrackingTest { val requestBody = getRequestBody(request) assertEquals("shoes", requestBody["search_term"]) - assertTrue(requestBody["items"]!!.contains("sl_campaign_id:camp-1")) - assertTrue(requestBody["items"]!!.contains("sl_campaign_owner:owner-a")) + assert(requestBody["items"]!!.contains("sl_campaign_id:camp-1")) + assert(requestBody["items"]!!.contains("sl_campaign_owner:owner-a")) } @Test @@ -118,6 +119,20 @@ class ConstructorIoResultsImpressionViewTrackingTest { assertEquals("shoes-123", requestBody["filter_value"]) } + @Test + fun trackResultsImpressionViewWithAnalyticsTags() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + + val items = listOf(ResultsImpressionItem("item-1", "Item One")) + val observer = ConstructorIo.trackResultsImpressionViewInternal(items, analyticsTags = mapOf("test" to "test1", "appVersion" to "150")).test() + observer.assertComplete() + val request = awaitRequest() + + val requestBody = getRequestBody(request) + assertEquals("{appVersion:150,appPlatform:Android,test:test1}", requestBody["analytics_tags"]) + } + @Test fun trackResultsImpressionView500() { val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") @@ -127,7 +142,7 @@ class ConstructorIoResultsImpressionViewTrackingTest { val observer = ConstructorIo.trackResultsImpressionViewInternal(items).test() observer.assertError { true } val request = awaitRequest() - assertTrue(request.path!!.startsWith("/v2/behavioral_action/impression_view")) + assert(request.path!!.startsWith("/v2/behavioral_action/impression_view")) } @Test @@ -143,7 +158,7 @@ class ConstructorIoResultsImpressionViewTrackingTest { observer.awaitDone(6, TimeUnit.SECONDS) observer.assertError(SocketTimeoutException::class.java) val request = awaitRequest() - assertTrue(request.path!!.startsWith("/v2/behavioral_action/impression_view")) + assert(request.path!!.startsWith("/v2/behavioral_action/impression_view")) } private fun awaitRequest(): RecordedRequest { From c61d3a448725770e778940c60866e5d2dd216b6a Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Tue, 30 Jun 2026 16:00:28 +0200 Subject: [PATCH 3/3] [REM-3440] Add shortcircuit before call when items array is empty. Add KDoc to trackResultsImpressionView, trackMediaImpressionClick and trackMediaImpressionView --- .../java/io/constructor/core/ConstructorIo.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index be0de603..fd964082 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -14,6 +14,7 @@ import io.constructor.data.model.browse.* import io.constructor.data.model.common.ResultGroup import io.constructor.data.model.common.TrackingItem import io.constructor.data.model.common.VariationsMap +import io.constructor.data.model.common.ResultsImpressionItem import io.constructor.data.model.conversion.ConversionRequestBody import io.constructor.data.model.purchase.PurchaseItem import io.constructor.data.model.purchase.PurchaseRequestBody @@ -26,7 +27,6 @@ import io.constructor.data.model.tracking.GenericResultClickRequestBody import io.constructor.data.model.tracking.ItemDetailLoadRequestBody import io.constructor.data.model.tracking.MediaImpressionRequestBody import io.constructor.data.model.tracking.ResultsImpressionViewRequestBody -import io.constructor.data.model.common.ResultsImpressionItem import io.constructor.injection.component.AppComponent import io.constructor.injection.component.DaggerAppComponent import io.constructor.injection.module.AppModule @@ -1951,6 +1951,14 @@ object ConstructorIo { /** * Tracks media impression view events. + * + * Example: + * ``` + * ConstructorIo.trackMediaImpressionView("banner-123", "placement-456") + * ``` + * @param bannerAdId the id of the banner ad viewed + * @param placementId the id of the placement + * @param analyticsTags Additional analytics tags to pass */ fun trackMediaImpressionView(bannerAdId: String, placementId: String, analyticsTags: Map? = null) { val completable = trackMediaImpressionViewInternal(bannerAdId, placementId, analyticsTags) @@ -1961,6 +1969,14 @@ object ConstructorIo { /** * Tracks media impression click events. + * + * Example: + * ``` + * ConstructorIo.trackMediaImpressionClick("banner-123", "placement-456") + * ``` + * @param bannerAdId the id of the banner ad clicked + * @param placementId the id of the placement + * @param analyticsTags Additional analytics tags to pass */ fun trackMediaImpressionClick(bannerAdId: String, placementId: String, analyticsTags: Map? = null) { val completable = trackMediaImpressionClickInternal(bannerAdId, placementId, analyticsTags) @@ -1971,8 +1987,19 @@ object ConstructorIo { /** * Tracks results impression view events (items that were viewably impressed per MRC guidelines). + * + * Example: + * ``` + * ConstructorIo.trackResultsImpressionView(items, "shoes") + * ``` + * @param items the list of items that were viewably impressed + * @param searchTerm the search term associated with the impression + * @param filterName the name of the filter applied + * @param filterValue the value of the filter applied + * @param analyticsTags Additional analytics tags to pass */ fun trackResultsImpressionView(items: List, searchTerm: String? = null, filterName: String? = null, filterValue: String? = null, analyticsTags: Map? = null) { + if (items.isEmpty()) return val completable = trackResultsImpressionViewInternal(items, searchTerm, filterName, filterValue, analyticsTags) disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, { t -> e("Results Impression View error: ${t.message}") @@ -2044,6 +2071,8 @@ object ConstructorIo { } internal fun trackResultsImpressionViewInternal(items: List, searchTerm: String? = null, filterName: String? = null, filterValue: String? = null, analyticsTags: Map? = null): Completable { + if (items.isEmpty()) return Completable.complete() + preferenceHelper.getSessionId(sessionIncrementHandler) val requestBody = ResultsImpressionViewRequestBody( items,