Attribute featured wishlists via UTM-tagged store visit - #1797
Conversation
📝 WalkthroughWalkthroughChangesWishlist attribution and authentication
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant FeaturedCtaButton
participant SteamWishlistService
participant SteamAuthentication
participant SteamStore
participant CMAPI
FeaturedCtaButton->>SteamWishlistService: addToWishlistAttributed(appId, campaignId)
SteamWishlistService->>SteamAuthentication: obtain Steam cookies and tokens
SteamWishlistService->>SteamStore: visit UTM URL and submit wishlist request
SteamStore-->>SteamWishlistService: session or request result
SteamWishlistService->>CMAPI: fallback wishlist operation
CMAPI-->>SteamWishlistService: Outcome
SteamWishlistService-->>FeaturedCtaButton: Outcome
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamWishlistService.kt (1)
65-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winGuard the JSON parse of the wishlist response.
Line 90 calls
JSONObject(body)on the raw response. Steam returns an HTML error page or an empty body on rate limiting and on session rejection.JSONObjectthen throws, and the exception unwinds to thecatchinaddToWishlistAttributed. The retry attempt is lost, and the log records an exception instead of the HTTP status.Parse defensively so the loop can retry.
♻️ Proposed refactor
- val ok = res.isSuccessful && JSONObject(body).optBoolean("success") + val ok = res.isSuccessful && runCatching { JSONObject(body).optBoolean("success") }.getOrDefault(false)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/service/SteamWishlistService.kt` around lines 65 - 99, Update the wishlist response handling in addToWishlistAttributed’s addtowishlist request to parse JSON defensively, treating empty or non-JSON bodies as an unsuccessful result instead of allowing JSONObject(body) to throw. Preserve the existing HTTP-status logging and return false so the surrounding retry loop can continue.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamWishlistService.kt`:
- Around line 59-62: Update the URL construction in the SteamWishlistService
flow to URL-encode campaignId before interpolating it into the utm_campaign
query parameter, preserving the existing attribution parameters. Correct the
mature-content cookie key in the cookie assembled alongside utmUrl to Steam’s
wants_mature_content name.
---
Nitpick comments:
In `@app/src/main/java/app/gamenative/service/SteamWishlistService.kt`:
- Around line 65-99: Update the wishlist response handling in
addToWishlistAttributed’s addtowishlist request to parse JSON defensively,
treating empty or non-JSON bodies as an unsuccessful result instead of allowing
JSONObject(body) to throw. Preserve the existing HTTP-status logging and return
false so the surrounding retry loop can continue.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c67622ea-94f0-4db4-9de0-0dada2c3f169
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/service/SteamWishlistService.ktapp/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt
| val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " + | ||
| "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1" | ||
| val utmUrl = "https://store.steampowered.com/app/$appId/" + | ||
| "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Encode campaignId in the URL and fix the mature-content cookie name.
Two problems in this block:
campaignIdcomes from remote featured JSON (FeaturedItem.campaignId). The code interpolates it into the query string without encoding. A value that contains&,#, or a space corrupts the UTM parameters, or makesRequest.Builder().url(...)throw. The attributed add then silently falls back to the plain CM add, so attribution is lost.wantsmatureconctentis misspelled. Steam useswants_mature_content. With the wrong cookie name, a mature-gated store page can return the age-check interstitial instead of the app page. Thedata-userinfoparse then fails andloggedInstays false, which burns both retry attempts.
🛠️ Proposed fix
val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " +
- "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1"
- val utmUrl = "https://store.steampowered.com/app/$appId/" +
- "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId"
+ "birthtime=0; lastagecheckage=1-January-1970; wants_mature_content=1"
+ val utmUrl = "https://store.steampowered.com/app/$appId/".toHttpUrl().newBuilder()
+ .addQueryParameter("utm_source", "gamenative")
+ .addQueryParameter("utm_medium", "app")
+ .addQueryParameter("utm_campaign", campaignId)
+ .build()
+ .toString()Note on the static analysis SSRF hints for Line 65 and Line 165: both hosts are hardcoded Steam endpoints, so this is not SSRF. The real risk is query-string corruption from the uncoded campaignId.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " + | |
| "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1" | |
| val utmUrl = "https://store.steampowered.com/app/$appId/" + | |
| "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId" | |
| val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " + | |
| "birthtime=0; lastagecheckage=1-January-1970; wants_mature_content=1" | |
| val utmUrl = "https://store.steampowered.com/app/$appId/".toHttpUrl().newBuilder() | |
| .addQueryParameter("utm_source", "gamenative") | |
| .addQueryParameter("utm_medium", "app") | |
| .addQueryParameter("utm_campaign", campaignId) | |
| .build() | |
| .toString() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/app/gamenative/service/SteamWishlistService.kt` around
lines 59 - 62, Update the URL construction in the SteamWishlistService flow to
URL-encode campaignId before interpolating it into the utm_campaign query
parameter, preserving the existing attribution parameters. Correct the
mature-content cookie key in the cookie assembled alongside utmUrl to Steam’s
wants_mature_content name.
Source: Linters/SAST tools
There was a problem hiding this comment.
3 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/service/SteamWishlistService.kt">
<violation number="1" location="app/src/main/java/app/gamenative/service/SteamWishlistService.kt:60">
P2: Typo in cookie key `wantsmatureconctent` — should be `wantsmaturecontent`. The word "content" is misspelled as "conctent". If Steam's storefront checks for this cookie by its correct key, the typo means the cookie will be ignored, which could affect age-gate or maturity-content behavior during the attributed wishlist session.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/service/SteamWishlistService.kt:61">
P2: Campaign IDs containing URL-reserved characters produce missing or incorrect UTM attribution; build the UTM URL with query parameters instead of concatenating remote data.</violation>
<violation number="3" location="app/src/main/java/app/gamenative/service/SteamWishlistService.kt:90">
P2: Successful Store adds are treated as failures because numeric `success: 1` does not satisfy `optBoolean`; accept the endpoint’s integer success value so an attributed add is not redundantly retried through CM.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| val utmUrl = "https://store.steampowered.com/app/$appId/" + | ||
| "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId" |
There was a problem hiding this comment.
P2: Campaign IDs containing URL-reserved characters produce missing or incorrect UTM attribution; build the UTM URL with query parameters instead of concatenating remote data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamWishlistService.kt, line 61:
<comment>Campaign IDs containing URL-reserved characters produce missing or incorrect UTM attribution; build the UTM URL with query parameters instead of concatenating remote data.</comment>
<file context>
@@ -34,6 +39,93 @@ object SteamWishlistService {
+ if (token.isEmpty()) token = refreshAccessToken() ?: return false
+ val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " +
+ "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1"
+ val utmUrl = "https://store.steampowered.com/app/$appId/" +
+ "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId"
+ var sessionId: String? = null
</file context>
| val utmUrl = "https://store.steampowered.com/app/$appId/" + | |
| "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId" | |
| val utmUrl = "https://store.steampowered.com/app/$appId/".toHttpUrl().newBuilder() | |
| .addQueryParameter("utm_source", "gamenative") | |
| .addQueryParameter("utm_medium", "app") | |
| .addQueryParameter("utm_campaign", campaignId) | |
| .build() | |
| .toString() |
| .build(), | ||
| ).execute().use { res -> | ||
| val body = res.body?.string().orEmpty() | ||
| val ok = res.isSuccessful && JSONObject(body).optBoolean("success") |
There was a problem hiding this comment.
P2: Successful Store adds are treated as failures because numeric success: 1 does not satisfy optBoolean; accept the endpoint’s integer success value so an attributed add is not redundantly retried through CM.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamWishlistService.kt, line 90:
<comment>Successful Store adds are treated as failures because numeric `success: 1` does not satisfy `optBoolean`; accept the endpoint’s integer success value so an attributed add is not redundantly retried through CM.</comment>
<file context>
@@ -34,6 +39,93 @@ object SteamWishlistService {
+ .build(),
+ ).execute().use { res ->
+ val body = res.body?.string().orEmpty()
+ val ok = res.isSuccessful && JSONObject(body).optBoolean("success")
+ Timber.tag(TAG).i("web addtowishlist http=${res.code} body=$body -> $ok")
+ return ok
</file context>
| val ok = res.isSuccessful && JSONObject(body).optBoolean("success") | |
| val ok = res.isSuccessful && JSONObject(body).let { | |
| it.optInt("success") == 1 || it.optBoolean("success") | |
| } |
| repeat(2) { attempt -> | ||
| if (token.isEmpty()) token = refreshAccessToken() ?: return false | ||
| val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " + | ||
| "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1" |
There was a problem hiding this comment.
P2: Typo in cookie key wantsmatureconctent — should be wantsmaturecontent. The word "content" is misspelled as "conctent". If Steam's storefront checks for this cookie by its correct key, the typo means the cookie will be ignored, which could affect age-gate or maturity-content behavior during the attributed wishlist session.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamWishlistService.kt, line 60:
<comment>Typo in cookie key `wantsmatureconctent` — should be `wantsmaturecontent`. The word "content" is misspelled as "conctent". If Steam's storefront checks for this cookie by its correct key, the typo means the cookie will be ignored, which could affect age-gate or maturity-content behavior during the attributed wishlist session.</comment>
<file context>
@@ -34,6 +39,93 @@ object SteamWishlistService {
+ repeat(2) { attempt ->
+ if (token.isEmpty()) token = refreshAccessToken() ?: return false
+ val cookie = "steamLoginSecure=$steamId%7C%7C${URLEncoder.encode(token, "UTF-8")}; " +
+ "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1"
+ val utmUrl = "https://store.steampowered.com/app/$appId/" +
+ "?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId"
</file context>
| "birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1" | |
| "birthtime=0; lastagecheckage=1-January-1970; wantsmaturecontent=1" |
…rshdalal#1797)" This reverts commit 45e59cd.
Description
Add headless UTM tag visit to steam wishlisting via app
Recording
Type of Change
Checklist
#code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.CONTRIBUTING.md.Summary by cubic
Attribute featured wishlist actions by opening a UTM-tagged Steam Store session and adding the app in that same session. Wishlist checks now work for private lists by using the store access token.
New Features
SteamWishlistService.addToWishlistAttributed(appId, campaignId)with fallback toaddToWishlist.https://store.steampowered.com/app/<id>?utm_source=gamenative&utm_medium=app&utm_campaign=<campaignId>, capturesessionid, then POST to/api/addtowishlist.FeaturedCtaButton: passcampaignIdintoInAppCta.forActionand use the attributed add for "WISHLIST".Bug Fixes
IWishlistService/GetWishlistwithaccess_tokenso private wishlists resolve; fall back when needed.SteamServicewhen missing or expired.Written for commit 25a4a11. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes