From 73b5b853091f2afc8df268e6404256ce67084a7c Mon Sep 17 00:00:00 2001 From: Tarek Alqaddy Date: Thu, 13 Aug 2026 20:03:06 +0300 Subject: [PATCH 1/5] overload trackSearchSubmit with new builder param --- README.md | 7 ++++ .../java/io/constructor/core/Constants.kt | 1 + .../java/io/constructor/core/ConstructorIo.kt | 27 +++++++++++++- .../data/builder/SearchSubmitData.kt | 36 +++++++++++++++++++ .../core/ConstructorIoTrackingTest.kt | 32 +++++++++++++++-- .../core/ConstructorioSegmentsTest.kt | 2 +- .../core/ConstructorioTestCellTest.kt | 2 +- 7 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt diff --git a/README.md b/README.md index d51d8eb6..a0aae515 100755 --- a/README.md +++ b/README.md @@ -529,6 +529,13 @@ ConstructorIo.trackAutocompleteSelect("Fashionable Toothpicks", "tooth", "Produc // Track when the user submits a search (searchTerm, originalQuery) ConstructorIo.trackSearchSubmit("toothpicks", "tooth") + +// Track when the user submits a search with additional parameters, i.e. analytics tags +// Request level analytics tags are merged with the default analytics tags passed on initialization +val request = SearchSubmitData.build("toothpicks", "tooth") { + setAnalyticsTags(mapOf("relatedSearchTerm" to "true")) +} +ConstructorIo.trackSearchSubmit(request) ``` ### Search Events diff --git a/library/src/main/java/io/constructor/core/Constants.kt b/library/src/main/java/io/constructor/core/Constants.kt index ae4473fb..209399b1 100755 --- a/library/src/main/java/io/constructor/core/Constants.kt +++ b/library/src/main/java/io/constructor/core/Constants.kt @@ -64,6 +64,7 @@ class Constants { const val GROUPS_MAX_DEPTH = "groups_max_depth" const val FILTER_GROUP_ID = "filters[group_id]" const val PRE_FILTER_EXPRESSION = "pre_filter_expression" + const val ANALYTICS_TAGS = "analytics_tags[%s]" } object QueryValues { diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index fd964082..b19cdfcb 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -1490,9 +1490,34 @@ object ConstructorIo { t -> e("Search Submit error: ${t.message}") })) } - internal fun trackSearchSubmitInternal(searchTerm: String, originalQuery: String, resultGroup: ResultGroup?): Completable { + + /** + * Tracks search submit events. + * + * Example: + * ``` + * val request = SearchSubmitData.build("toothpicks", "tooth") { + * setResultGroup(ResultGroup("Canned Goods", "canned-goods")) + * setAnalyticsTags(mapOf("relatedSearchTerm" to "true")) + * } + * ConstructorIo.trackSearchSubmit(request) + * ``` + * @param request the search submit request object holding all of the tracking parameters + */ + fun trackSearchSubmit(request: SearchSubmitData) { + var completable = trackSearchSubmitInternal(request.searchTerm, request.originalQuery, request.resultGroup, request.analyticsTags) + disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({ + context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to request.searchTerm) + }, { + t -> e("Search Submit error: ${t.message}") + })) + } + internal fun trackSearchSubmitInternal(searchTerm: String, originalQuery: String, resultGroup: ResultGroup?, analyticsTags: Map? = null): Completable { preferenceHelper.getSessionId(sessionIncrementHandler) val encodedParams: ArrayList> = getEncodedParams(groupId = resultGroup?.groupId, groupDisplayName = resultGroup?.displayName) + mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags)?.forEach { analyticsTag -> + encodedParams.add(Constants.QueryConstants.ANALYTICS_TAGS.format(analyticsTag.key).urlEncode() to analyticsTag.value.urlEncode()) + } return dataManager.trackSearchSubmit(searchTerm, arrayOf( Constants.QueryConstants.ORIGINAL_QUERY to originalQuery, diff --git a/library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt b/library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt new file mode 100644 index 00000000..0f661a64 --- /dev/null +++ b/library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt @@ -0,0 +1,36 @@ +package io.constructor.data.builder + +import io.constructor.data.model.common.ResultGroup + +/** + * Create a Search Submit tracking request object utilizing a builder + */ +class SearchSubmitData( + val searchTerm: String, + val originalQuery: String, + val resultGroup: ResultGroup? = null, + val analyticsTags: Map? = null, +) { + private constructor(builder: Builder) : this( + builder.searchTerm, + builder.originalQuery, + builder.resultGroup, + builder.analyticsTags, + ) + + companion object { + inline fun build(searchTerm: String, originalQuery: String, block: Builder.() -> Unit = {}) = Builder(searchTerm, originalQuery).apply(block).build() + } + + class Builder( + val searchTerm: String, + val originalQuery: String + ) { + var resultGroup: ResultGroup? = null + var analyticsTags: Map? = null + + fun setResultGroup(resultGroup: ResultGroup): Builder = apply { this.resultGroup = resultGroup } + fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } + fun build(): SearchSubmitData = SearchSubmitData(this) + } +} diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt index 2bd1be12..bc500060 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt @@ -6,6 +6,7 @@ import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.common.ResultGroup import io.constructor.data.model.purchase.PurchaseItem import io.constructor.data.model.common.TrackingItem +import io.constructor.data.builder.SearchSubmitData import io.constructor.test.createTestDataManager import io.constructor.util.RxSchedulersOverrideRule import io.mockk.every @@ -340,10 +341,35 @@ class ConstructorIoTrackingTest { val observer = ConstructorIo.trackSearchSubmitInternal("titanic", "tit", null).test() observer.assertComplete() val request = mockServer.takeRequest() - val path = "/autocomplete/titanic/search?original_query=tit&tr=search&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&analytics_tags%5BappVersion%5D=123&analytics_tags%5BappPlatform%5D=Android&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" assert(request.path!!.startsWith(path)) } + @Test + fun trackSearchSubmitWithAnalyticsTags() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val observer = ConstructorIo.trackSearchSubmitInternal("titanic", "tit", null, mapOf("test" to "test1", "appVersion" to "150")).test() + observer.assertComplete() + val request = mockServer.takeRequest() + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&analytics_tags%5BappVersion%5D=150&analytics_tags%5BappPlatform%5D=Android&analytics_tags%5Btest%5D=test1&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" + assert(request.path!!.startsWith(path)) + } + + @Test + fun trackSearchSubmitWithRequestBuilder() { + val mockResponse = MockResponse().setResponseCode(204) + mockServer.enqueue(mockResponse) + val request = SearchSubmitData.build("titanic", "tit") { + setResultGroup(ResultGroup("Movies", "group_id")) + setAnalyticsTags(mapOf("relatedSearchTerm" to "true")) + } + ConstructorIo.trackSearchSubmit(request) + val recordedRequest = mockServer.takeRequest() + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&group%5Bgroup_id%5D=group_id&group%5Bdisplay_name%5D=Movies&analytics_tags%5BappVersion%5D=123&analytics_tags%5BappPlatform%5D=Android&analytics_tags%5BrelatedSearchTerm%5D=true&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" + assert(recordedRequest.path!!.startsWith(path)) + } + @Test fun trackSearchSubmit500() { val mockResponse = MockResponse().setResponseCode(500).setBody("Internal server error") @@ -351,7 +377,7 @@ class ConstructorIoTrackingTest { val observer = ConstructorIo.trackSearchSubmitInternal("titanic", "tit", null).test() observer.assertError { true } val request = mockServer.takeRequest() - val path = "/autocomplete/titanic/search?original_query=tit&tr=search&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&analytics_tags%5BappVersion%5D=123&analytics_tags%5BappPlatform%5D=Android&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" assert(request.path!!.startsWith(path)) } @@ -363,7 +389,7 @@ class ConstructorIoTrackingTest { val observer = ConstructorIo.trackSearchSubmitInternal("titanic", "tit", null).test() observer.assertError(SocketTimeoutException::class.java) val request = mockServer.takeRequest() - val path = "/autocomplete/titanic/search?original_query=tit&tr=search&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&analytics_tags%5BappVersion%5D=123&analytics_tags%5BappPlatform%5D=Android&key=copper-key&i=wacko-the-guid&ui=player-three&s=67&c=cioand-2.44.0&_dt=" assert(request.path!!.startsWith(path)) } diff --git a/library/src/test/java/io/constructor/core/ConstructorioSegmentsTest.kt b/library/src/test/java/io/constructor/core/ConstructorioSegmentsTest.kt index 18ccf87c..21752653 100644 --- a/library/src/test/java/io/constructor/core/ConstructorioSegmentsTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorioSegmentsTest.kt @@ -109,7 +109,7 @@ class ConstructorioSegmentsTest { val observer = ConstructorIo.trackSearchSubmitInternal("titanic", "tit", null).test() observer.assertComplete() val request = mockServer.takeRequest() - val path = "/autocomplete/titanic/search?original_query=tit&tr=search&key=aluminium-key&i=koopa-the-guid&ui=player-two&s=14&us=mobile&us=COUNTRY_US&c=cioand-2.44.0&_dt=" + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&analytics_tags%5BappVersion%5D=123&analytics_tags%5BappPlatform%5D=Android&key=aluminium-key&i=koopa-the-guid&ui=player-two&s=14&us=mobile&us=COUNTRY_US&c=cioand-2.44.0&_dt=" assert(request.path!!.startsWith(path)) } diff --git a/library/src/test/java/io/constructor/core/ConstructorioTestCellTest.kt b/library/src/test/java/io/constructor/core/ConstructorioTestCellTest.kt index c1ad1cf1..5452b991 100644 --- a/library/src/test/java/io/constructor/core/ConstructorioTestCellTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorioTestCellTest.kt @@ -110,7 +110,7 @@ class ConstructorioTestCellTest { val observer = ConstructorIo.trackSearchSubmitInternal("titanic", "tit", null).test() observer.assertComplete() val request = mockServer.takeRequest() - val path = "/autocomplete/titanic/search?original_query=tit&tr=search&key=aluminium-key&i=koopa-the-guid&ui=player-two&s=14&ef-cellone=vanilla&ef-celltwo=whipped-cream&c=cioand-2.44.0&_dt="; + val path = "/autocomplete/titanic/search?original_query=tit&tr=search&analytics_tags%5BappVersion%5D=123&analytics_tags%5BappPlatform%5D=Android&key=aluminium-key&i=koopa-the-guid&ui=player-two&s=14&ef-cellone=vanilla&ef-celltwo=whipped-cream&c=cioand-2.44.0&_dt="; assert(request.path!!.startsWith(path)) } From baeb99d9decc185498edc3aa48b674c28a60e929 Mon Sep 17 00:00:00 2001 From: Tarek Alqaddy Date: Mon, 17 Aug 2026 17:03:43 +0300 Subject: [PATCH 2/5] address comments --- library/src/main/java/io/constructor/core/ConstructorIo.kt | 7 ++++--- .../{SearchSubmitData.kt => SearchSubmitTrackingData.kt} | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) rename library/src/main/java/io/constructor/data/builder/{SearchSubmitData.kt => SearchSubmitTrackingData.kt} (83%) diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index b19cdfcb..0173f6cf 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -1505,18 +1505,19 @@ object ConstructorIo { * @param request the search submit request object holding all of the tracking parameters */ fun trackSearchSubmit(request: SearchSubmitData) { - var completable = trackSearchSubmitInternal(request.searchTerm, request.originalQuery, request.resultGroup, request.analyticsTags) + val completable = trackSearchSubmitInternal(request.searchTerm, request.originalQuery, request.resultGroup, request.analyticsTags) disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({ context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to request.searchTerm) }, { t -> e("Search Submit error: ${t.message}") })) } + internal fun trackSearchSubmitInternal(searchTerm: String, originalQuery: String, resultGroup: ResultGroup?, analyticsTags: Map? = null): Completable { preferenceHelper.getSessionId(sessionIncrementHandler) val encodedParams: ArrayList> = getEncodedParams(groupId = resultGroup?.groupId, groupDisplayName = resultGroup?.displayName) - mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags)?.forEach { analyticsTag -> - encodedParams.add(Constants.QueryConstants.ANALYTICS_TAGS.format(analyticsTag.key).urlEncode() to analyticsTag.value.urlEncode()) + mergeAnalyticsTags(configMemoryHolder.defaultAnalyticsTags, analyticsTags)?.forEach { (key, value) -> + encodedParams.add(Constants.QueryConstants.ANALYTICS_TAGS.format(key).urlEncode() to value.urlEncode()) } return dataManager.trackSearchSubmit(searchTerm, arrayOf( diff --git a/library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt b/library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt similarity index 83% rename from library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt rename to library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt index 0f661a64..b1dfe669 100644 --- a/library/src/main/java/io/constructor/data/builder/SearchSubmitData.kt +++ b/library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt @@ -19,7 +19,11 @@ class SearchSubmitData( ) companion object { - inline fun build(searchTerm: String, originalQuery: String, block: Builder.() -> Unit = {}) = Builder(searchTerm, originalQuery).apply(block).build() + inline fun build( + searchTerm: String, + originalQuery: String, + block: Builder.() -> Unit = {} + ) = Builder(searchTerm, originalQuery).apply(block).build() } class Builder( From 05896be15eb030f0332fa55543906042d74d43b3 Mon Sep 17 00:00:00 2001 From: Tarek Alqaddy Date: Tue, 18 Aug 2026 14:58:05 +0300 Subject: [PATCH 3/5] Rename class --- library/src/main/java/io/constructor/core/ConstructorIo.kt | 2 +- .../io/constructor/data/builder/SearchSubmitTrackingData.kt | 4 ++-- .../java/io/constructor/core/ConstructorIoTrackingTest.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 0173f6cf..1a4c1275 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -1504,7 +1504,7 @@ object ConstructorIo { * ``` * @param request the search submit request object holding all of the tracking parameters */ - fun trackSearchSubmit(request: SearchSubmitData) { + fun trackSearchSubmit(request: SearchSubmitTrackingData) { val completable = trackSearchSubmitInternal(request.searchTerm, request.originalQuery, request.resultGroup, request.analyticsTags) disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({ context.broadcastIntent(Constants.EVENT_QUERY_SENT, Constants.EXTRA_TERM to request.searchTerm) diff --git a/library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt b/library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt index b1dfe669..ab086396 100644 --- a/library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt +++ b/library/src/main/java/io/constructor/data/builder/SearchSubmitTrackingData.kt @@ -5,7 +5,7 @@ import io.constructor.data.model.common.ResultGroup /** * Create a Search Submit tracking request object utilizing a builder */ -class SearchSubmitData( +class SearchSubmitTrackingData( val searchTerm: String, val originalQuery: String, val resultGroup: ResultGroup? = null, @@ -35,6 +35,6 @@ class SearchSubmitData( fun setResultGroup(resultGroup: ResultGroup): Builder = apply { this.resultGroup = resultGroup } fun setAnalyticsTags(analyticsTags: Map): Builder = apply { this.analyticsTags = analyticsTags } - fun build(): SearchSubmitData = SearchSubmitData(this) + fun build(): SearchSubmitTrackingData = SearchSubmitTrackingData(this) } } diff --git a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt index bc500060..ce72ce18 100755 --- a/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt +++ b/library/src/test/java/io/constructor/core/ConstructorIoTrackingTest.kt @@ -6,7 +6,7 @@ import io.constructor.data.memory.ConfigMemoryHolder import io.constructor.data.model.common.ResultGroup import io.constructor.data.model.purchase.PurchaseItem import io.constructor.data.model.common.TrackingItem -import io.constructor.data.builder.SearchSubmitData +import io.constructor.data.builder.SearchSubmitTrackingData import io.constructor.test.createTestDataManager import io.constructor.util.RxSchedulersOverrideRule import io.mockk.every @@ -360,7 +360,7 @@ class ConstructorIoTrackingTest { fun trackSearchSubmitWithRequestBuilder() { val mockResponse = MockResponse().setResponseCode(204) mockServer.enqueue(mockResponse) - val request = SearchSubmitData.build("titanic", "tit") { + val request = SearchSubmitTrackingData.build("titanic", "tit") { setResultGroup(ResultGroup("Movies", "group_id")) setAnalyticsTags(mapOf("relatedSearchTerm" to "true")) } From 447342c538b57589e43d4b42e0926b85965fb0f2 Mon Sep 17 00:00:00 2001 From: Tarek Alqaddy Date: Tue, 18 Aug 2026 19:20:29 +0300 Subject: [PATCH 4/5] Fix docs and readme --- README.md | 2 +- library/src/main/java/io/constructor/core/ConstructorIo.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0aae515..87c10ed5 100755 --- a/README.md +++ b/README.md @@ -532,7 +532,7 @@ ConstructorIo.trackSearchSubmit("toothpicks", "tooth") // Track when the user submits a search with additional parameters, i.e. analytics tags // Request level analytics tags are merged with the default analytics tags passed on initialization -val request = SearchSubmitData.build("toothpicks", "tooth") { +val request = SearchSubmitTrackingData.build("toothpicks", "tooth") { setAnalyticsTags(mapOf("relatedSearchTerm" to "true")) } ConstructorIo.trackSearchSubmit(request) diff --git a/library/src/main/java/io/constructor/core/ConstructorIo.kt b/library/src/main/java/io/constructor/core/ConstructorIo.kt index 1a4c1275..73988155 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -1496,7 +1496,7 @@ object ConstructorIo { * * Example: * ``` - * val request = SearchSubmitData.build("toothpicks", "tooth") { + * val request = SearchSubmitTrackingData.build("toothpicks", "tooth") { * setResultGroup(ResultGroup("Canned Goods", "canned-goods")) * setAnalyticsTags(mapOf("relatedSearchTerm" to "true")) * } From 0986971bf86195febd01fc731e4664cb684927e3 Mon Sep 17 00:00:00 2001 From: Tarek Alqaddy Date: Wed, 19 Aug 2026 16:01:22 +0300 Subject: [PATCH 5/5] docs --- library/src/main/java/io/constructor/core/ConstructorIo.kt | 5 ++++- 1 file changed, 4 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 73988155..bcd5a5c7 100755 --- a/library/src/main/java/io/constructor/core/ConstructorIo.kt +++ b/library/src/main/java/io/constructor/core/ConstructorIo.kt @@ -1502,7 +1502,10 @@ object ConstructorIo { * } * ConstructorIo.trackSearchSubmit(request) * ``` - * @param request the search submit request object holding all of the tracking parameters + * @param request the search 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 trackSearchSubmit(request: SearchSubmitTrackingData) { val completable = trackSearchSubmitInternal(request.searchTerm, request.originalQuery, request.resultGroup, request.analyticsTags)