Skip to content

[CDX-475] Implement ASA tracking events - #169

Open
Mudaafi wants to merge 5 commits into
masterfrom
cdx-475-asa-tracking-events
Open

Mudaafi wants to merge 5 commits into
masterfrom
cdx-475-asa-tracking-events

Conversation

@Mudaafi

@Mudaafi Mudaafi commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This implementation mirrors the swift one: Constructor-io/constructorio-client-swift#278

  • Add 6 Agent Search Assistant (ASA) tracking events: trackAgentSubmit, trackAgentResultLoadStarted, trackAgentResultLoadFinished, trackAgentResultClick, trackAgentResultView, trackAgentSearchSubmit
  • Add request body models, API paths, Retrofit endpoints, DataManager pass-throughs, and public fire-and-forget methods
  • Add unit tests covering success, error, timeout, optional params, analytics tags, and 100-item cap
  • Fix quiz integration tests to pass quiz_session_id with answers per updated API requirement
  • Add README documentation for ASA events

Test plan

  • Run ConstructorIoAgentTrackingTest (23 tests covering all 6 events)
  • Run ConstructorIoIntegrationQuizTest to verify quiz test fix
  • Verify tracking calls hit correct /v2/behavioral_action/ endpoints

Note: There are some request/response mismatches due to searchability issues. I've opened a thread to investigate it.

Add 6 Agent Search Assistant tracking events:
- trackAgentSubmit
- trackAgentResultLoadStarted
- trackAgentResultLoadFinished
- trackAgentResultClick
- trackAgentResultView
- trackAgentSearchSubmit

Also fix quiz integration tests to pass quiz_session_id
with answers per updated API requirement.
@Mudaafi
Mudaafi marked this pull request as ready for review July 13, 2026 14:56
@Mudaafi
Mudaafi requested a review from a team as a code owner July 13, 2026 14:56
@Mudaafi
Mudaafi requested review from a team and Copilot July 13, 2026 14:56
constructor-claude-bedrock[bot]

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds support for Agent Search Assistant (ASA) behavioral tracking in the Android SDK, wiring new ASA event APIs through request models → Retrofit endpoints → DataManager → public ConstructorIo fire-and-forget methods, and updates quiz integration tests/docs to match current API requirements.

Changes:

  • Added 6 ASA tracking events (trackAgentSubmit, trackAgentResultLoadStarted, trackAgentResultLoadFinished, trackAgentResultClick, trackAgentResultView, trackAgentSearchSubmit) including request-body models and Retrofit/DataManager plumbing.
  • Added a dedicated unit test suite (ConstructorIoAgentTrackingTest) covering success/error/timeout, optional params, analytics tags merging, and 100-item cap behavior.
  • Updated quiz integration tests to include quizVersionId/quizSessionId in requests and updated README documentation to include ASA event examples.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Documents new ASA tracking event APIs and adds “Agent Events” to the tracking taxonomy.
library/src/test/java/io/constructor/core/ConstructorIoIntegrationQuizTest.kt Updates quiz integration tests to include quizVersionId/quizSessionId and adjust answer payloads.
library/src/test/java/io/constructor/core/ConstructorIoAgentTrackingTest.kt Adds unit tests validating ASA tracking request shapes and error/timeout behavior.
library/src/main/java/io/constructor/data/remote/ConstructorApi.kt Introduces Retrofit endpoints for the new ASA behavioral action routes.
library/src/main/java/io/constructor/data/remote/ApiPaths.kt Adds API path constants for the 6 ASA tracking endpoints.
library/src/main/java/io/constructor/data/model/tracking/AgentSubmitRequestBody.kt Adds request body model for ASA submit tracking.
library/src/main/java/io/constructor/data/model/tracking/AgentResultLoadStartedRequestBody.kt Adds request body model for ASA “load started” tracking.
library/src/main/java/io/constructor/data/model/tracking/AgentResultLoadFinishedRequestBody.kt Adds request body model for ASA “load finished” tracking.
library/src/main/java/io/constructor/data/model/tracking/AgentResultClickRequestBody.kt Adds request body model for ASA result click tracking.
library/src/main/java/io/constructor/data/model/tracking/AgentResultViewRequestBody.kt Adds request body model for ASA result view tracking (with items list).
library/src/main/java/io/constructor/data/model/tracking/AgentSearchSubmitRequestBody.kt Adds request body model for ASA search submit tracking.
library/src/main/java/io/constructor/data/DataManager.kt Adds pass-through methods to call the new Retrofit ASA endpoints.
library/src/main/java/io/constructor/core/ConstructorIo.kt Adds public ASA tracking APIs plus internal implementations that build request bodies and dispatch to DataManager.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2407 to +2409

return dataManager.trackAgentSubmit(requestBody)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Mudaafi is there a reason to add section as a query param for trackAgentResultClick and trackAgentResultView only? 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not particularly. I didn't realize they would cater to different sections. I'll add it in since the API supports it.

Comment on lines +2447 to +2449

return dataManager.trackAgentResultLoadStarted(requestBody)
}
Comment on lines +2489 to +2491

return dataManager.trackAgentResultLoadFinished(requestBody)
}
Comment on lines +2634 to +2636

return dataManager.trackAgentSearchSubmit(requestBody)
}

@TarekAlQaddy TarekAlQaddy left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Everything looks good from my side! Could you please add tests for the existence of userId and segments in the requests body?

Comment on lines +2407 to +2409

return dataManager.trackAgentSubmit(requestBody)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Mudaafi is there a reason to add section as a query param for trackAgentResultClick and trackAgentResultView only? 👀

Section is already included in the request body — no need to
pass it as a query parameter. Aligns with Swift SDK behavior.
constructor-claude-bedrock[bot]

This comment was marked as outdated.

The API supports section as a query parameter on every agent
behavioral endpoint, so pass it alongside the body field for
consistency with the other sectioned tracking calls.
constructor-claude-bedrock[bot]

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

@constructor-claude-bedrock constructor-claude-bedrock Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This PR implements 6 ASA (Agent Search Assistant) tracking events, following the established patterns in the codebase well overall — but there are a few issues worth addressing before merging.

Inline comments: 7 discussions added

Overall Assessment: ⚠️ Needs Work

*/
fun trackAgentSubmit(intent: String, sectionName: String? = null, analyticsTags: Map<String, String>? = null) {
val completable = trackAgentSubmitInternal(intent, sectionName, analyticsTags)
disposable.add(completable.subscribeOn(Schedulers.io()).subscribe({}, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

The new code writes:

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

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

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

internal fun trackAgentResultViewInternal(intent: String, searchResultId: String, numResultsViewed: Int, items: List<ResultsImpressionItem>? = null, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null): Completable {
preferenceHelper.getSessionId(sessionIncrementHandler)
val section = sectionName ?: preferenceHelper.defaultItemSection
val cappedItems = items?.take(100)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

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

* @param intentResultId the intent result identifier
* @param analyticsTags additional analytics tags to pass
*/
fun trackAgentResultView(intent: String, searchResultId: String, numResultsViewed: Int, items: List<ResultsImpressionItem>? = null, sectionName: String? = null, intentResultId: String? = null, analyticsTags: Map<String, String>? = null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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


@JsonClass(generateAdapter = true)
data class AgentSubmitRequestBody(
@Json(name = "intent") val intent: String,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: AgentSubmitRequestBody is missing the intent_result_id field that every other Agent request body includes. Looking at the Swift mirror PR and the other five bodies, all of them carry intentResultId. If trackAgentSubmit genuinely doesn't support it at the API level that's fine, but it should be explicitly documented in the KDoc of trackAgentSubmit. If it does need it, add the field and propagate the parameter through ConstructorIo.trackAgentSubmit and trackAgentSubmitInternal.

@Json(name = "ui") val ui: String?,
@Json(name = "us") val us: List<String?>,
@Json(name = "analytics_tags") val analyticsTags: Map<String, String>?,
@Json(name = "_dt") val _dt: Long?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The field name _dt on a Kotlin property starts with an underscore, which is a Kotlin naming convention violation (used to signal private backing properties). The same pattern exists in other existing request bodies in the codebase so this is pre-existing, but since all six new request bodies replicate it, it's worth flagging. Consider an alias:

@Json(name = "_dt") val dt: Long?

This avoids suppressing potential lint warnings across six new files.

val request = awaitRequest()

val requestBody = getRequestBody(request)
assert(requestBody["items"]!!.contains("item_id:item-100"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: The 100-item cap test (trackAgentResultViewCapsItemsAt100) uses assert(!requestBody["items"]!!.contains("item_id:item-101")). This assertion is fragile — item_id:item-10 is a substring of item_id:item-101, so the negative assertion could false-pass if the serialisation format changes slightly. Use a more robust check, e.g. verify the number of item_id: occurrences in the string:

val itemCount = requestBody["items"]!!.split("item_id:").size - 1
assertEquals(100, itemCount)

Or at minimum assert !contains("item_id:item-101,") with a trailing delimiter.

}

@Test
fun trackAgentSubmitTimeout() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The timeout tests set setBodyDelay(5, TimeUnit.SECONDS) and then awaitDone(6, TimeUnit.SECONDS), but the default OkHttp read timeout configured for tests should already fire before 5 s. Double-check the test DataManager/OkHttpClient configuration to confirm the timeout is shorter than 5 s, otherwise these tests will be slow in CI. If the actual timeout is 1 s (common default), reduce the delay to e.g. 2 s and awaitDone to 3 s — consistent with patterns in ConstructorIoMediaTrackingTest.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants