Return TrackingResult with tracking token from collectAndSend()#14
Return TrackingResult with tracking token from collectAndSend()#14
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
collectAndSend() and sendDeviceData() now return Result<TrackingResult> with an opaque trackingToken for use with the minFraud API. This avoids exposing ServerResponse internals and treats the token as opaque, since its format may change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make internal: 18 model classes, ServerResponse, SdkConfig host constants, collectDeviceData(), and sendDeviceData(). Only TrackingResult, DeviceTracker, and SdkConfig+Builder remain public. Simplify sample app to use only collectAndSend(), removing DeviceData inspection, reflection, and serialization dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello @oschwald, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request refines the SDK's public interface by making numerous internal components private, thereby simplifying its usage and reducing potential for misuse. It also enhances the utility of the primary data submission method by returning a specific Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a breaking change to the SDK's public API, primarily by modifying the collectAndSend() and sendDeviceData() methods to return a Result<TrackingResult> which includes a trackingToken for minFraud API integration, replacing the previous Result<Unit>. The collectDeviceData() and sendDeviceData() methods have been made internal, simplifying the public interface to collectAndSend(). These changes are reflected across the CHANGELOG, CLAUDE.md, and README.md documentation, with updated code examples in the README and the sample application. Additionally, many internal data model classes and SdkConfig constants were changed from public to internal visibility to reduce the public API surface. Review comments suggested improving exception handling in DeviceTracker.kt by using more specific exception types like IOException or custom exceptions when the tracking token is missing or blank from the server response.
| response.storedID | ||
| ?: error("Server response missing tracking token") |
There was a problem hiding this comment.
There was a problem hiding this comment.
The I/O operation itself succeeded — the server responded, it just didn't include the expected field. This is a protocol/contract violation, not an I/O error, so IllegalStateException (via error()) is the right fit. Additionally, this is wrapped by mapCatching into a Result.failure, so callers never catch the exception type directly — they pattern-match on the Result.
| internal companion object { | ||
| /** Default IPv6 server host */ | ||
| public const val DEFAULT_IPV6_HOST: String = "d-ipv6.mmapiws.com" | ||
| internal const val DEFAULT_IPV6_HOST: String = "d-ipv6.mmapiws.com" | ||
|
|
||
| /** Default IPv4 server host */ | ||
| public const val DEFAULT_IPV4_HOST: String = "d-ipv4.mmapiws.com" | ||
| internal const val DEFAULT_IPV4_HOST: String = "d-ipv4.mmapiws.com" | ||
|
|
||
| /** API endpoint path */ | ||
| public const val ENDPOINT_PATH: String = "/device/android" | ||
| internal const val ENDPOINT_PATH: String = "/device/android" |
| val trackingToken: String, | ||
| ) { | ||
| init { | ||
| require(trackingToken.isNotBlank()) { "Tracking token must not be blank" } |
There was a problem hiding this comment.
require throwing IllegalArgumentException is idiomatic Kotlin for precondition checks. A custom exception type would add a public API surface element for something that's only thrown when the SDK itself has a bug (server returned a blank token). Since it's wrapped in Result.failure, callers handle it via onFailure rather than catching specific exception types.
horgh
left a comment
There was a problem hiding this comment.
LGTM. The robots had some feedback though 🤖
|
@gemini-code-assist review |
There was a problem hiding this comment.
Code Review
This pull request is a great improvement to the SDK. It introduces the TrackingResult with a trackingToken, which is a valuable feature for integrating with the minFraud API. The reduction of the public API surface by making many classes and functions internal is a solid architectural change that improves encapsulation and maintainability. The updates to the documentation, sample app, and ProGuard rules are all consistent with these changes. I have one suggestion to make the collectAndSend function more robust by ensuring it always returns a Result.
| val deviceData = collectDeviceData() | ||
| return sendDeviceData(deviceData) |
There was a problem hiding this comment.
The collectAndSend function should be more robust by always returning a Result and not throwing exceptions (except for CancellationException). Currently, if collectDeviceData() throws an exception, collectAndSend will also throw. For a public API function that is declared to return a Result, it's better to catch potential exceptions and wrap them in a Result.failure. This makes the function's contract clearer and simplifies error handling for consumers.
return try {
val deviceData = collectDeviceData()
sendDeviceData(deviceData)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}There was a problem hiding this comment.
Good catch — wrapping only the call site in startAutomaticCollection() fixed the symptom but not the root cause. Moved the try/catch into collectAndSend() itself so the Result contract is honored for all callers, and simplified startAutomaticCollection() back to just .fold().
Construct TrackingResult (which validates via require(isNotBlank)) before calling storedIDStorage.save(). Previously a blank token would be persisted and then fail validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
collectAndSend() calls collectDeviceData() which can throw before returning a Result. An uncaught exception kills the coroutine and stops automatic collection permanently. Wrap in try/catch while letting CancellationException propagate for structured concurrency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove "Manual Data Collection" section since collectDeviceData() is now internal - Replace Log.d() with sendToBackend() in examples to guide users toward passing the token to their backend for minFraud integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only keep TrackingResult (the sole public model class) in consumer rules. Internal model classes like DeviceData and ServerResponse can now be shrunk by R8 in consumer apps. Serialization rules remain broad since internal classes still need them at runtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These color resources are unused in the sample app. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The catch block `catch (e: Exception)` swallowed CancellationException, breaking structured concurrency. When shutdown() cancels the coroutine scope, in-flight HTTP calls would not propagate cancellation correctly. This applies the same re-throw pattern already used at both sites in DeviceTracker.kt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The data class default toString() would expose the full tracking token in logs, crash reports, and debug output. Override it with a redacted version to prevent accidental token leakage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The IPv4 request result was silently discarded, making it impossible to diagnose persistent IPv4 endpoint failures. Log success/failure at debug level when enableLogging is true. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove sendDeviceData() from CHANGELOG first bullet since it is no longer part of the public API - Update CLAUDE.md DeviceData section to reflect internal visibility - Remove "Keeps Ktor classes" from CLAUDE.md ProGuard section (those rules are in proguard-rules.pro, not consumer-rules.pro) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a terminology section clarifying the distinction: "stored ID" is the server-generated value used internally throughout the SDK, while "tracking token" is the public-facing name exposed to consumers via TrackingResult. They are currently the same value but the abstraction allows them to diverge. Also fix the dual-request flow description which incorrectly said "tracking token". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
21d6c29 to
1a5452e
Compare
There was a problem hiding this comment.
Issue 1: mapCatching swallows CancellationException (Bug)
In DeviceTracker.kt:85, sendDeviceData uses mapCatching:
internal suspend fun sendDeviceData(deviceData: DeviceData): Result<TrackingResult> =
apiClient.sendDeviceData(deviceData).mapCatching { response ->
...
}
mapCatching internally uses runCatching which catches Throwable — including CancellationException. If the coroutine is cancelled while executing inside the mapCatching lambda
(e.g., between getting the server response and completing the storage save), the CancellationException gets wrapped in Result.failure() instead of propagating.
This also makes the catch (e: CancellationException) { throw e } on line 95-96 inside the storage try-catch effectively useless — the rethrown CancellationException just gets
caught again by mapCatching.
This is a well-known Kotlin coroutines footgun. Consider replacing mapCatching with explicit handling, e.g.:
internal suspend fun sendDeviceData(deviceData: DeviceData): Result<TrackingResult> {
val response = apiClient.sendDeviceData(deviceData)
.getOrElse { return Result.failure(it) }
val token = response.storedID
?: return Result.failure(IllegalStateException("Server response missing tracking token"))
val result = TrackingResult(trackingToken = token)
try {
storedIDStorage.save(token)
...
} catch (e: CancellationException) {
throw e
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
...
}
return Result.success(result)
}
There was a problem hiding this comment.
Unrelated to the current changes, but Claude noticed this:
Issue 2: prettyPrint = true in production JSON (Performance)
DeviceApiClient.kt:46:
private val json =
Json {
prettyPrint = true
...
}
Every API request sent from the SDK uses pretty-printed JSON, adding unnecessary whitespace to every payload. On mobile devices where bandwidth and battery matter, this should
probably be false (or omitted — it defaults to false). If it's there for debugging, consider gating it behind config.enableLogging.
mapCatching internally catches Throwable (including CancellationException), which breaks structured concurrency cancellation. Replace with getOrElse/early-return so only CancellationException propagates and all other failures are wrapped in Result.failure(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
prettyPrint adds unnecessary whitespace to every API request payload, wasting bandwidth on mobile devices. It defaults to false, which is the appropriate setting for production use. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
collectAndSend()now returnsResult<TrackingResult>instead ofResult<Unit>, exposing an opaquetrackingTokenfor use with the minFraud API's/device/tracking_tokenfieldServerResponse,SdkConfighost constants,collectDeviceData(), andsendDeviceData()— consumers only needcollectAndSend()Test plan
./gradlew :device-sdk:test— all unit tests pass (includes newDeviceTrackerTest)./gradlew :sample:assembleDebug— sample app builds./gradlew detekt ktlintCheck— linting passes🤖 Generated with Claude Code