Skip to content

Attribute featured wishlists via UTM-tagged store visit - #1797

Merged
utkarshdalal merged 2 commits into
masterfrom
wishlist-utm
Aug 8, 2026
Merged

Attribute featured wishlists via UTM-tagged store visit#1797
utkarshdalal merged 2 commits into
masterfrom
wishlist-utm

Conversation

@utkarshdalal

@utkarshdalal utkarshdalal commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Description

Add headless UTM tag visit to steam wishlisting via app

Recording

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #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.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in 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

    • Add SteamWishlistService.addToWishlistAttributed(appId, campaignId) with fallback to addToWishlist.
    • Headless visit to https://store.steampowered.com/app/<id>?utm_source=gamenative&utm_medium=app&utm_campaign=<campaignId>, capture sessionid, then POST to /api/addtowishlist.
    • FeaturedCtaButton: pass campaignId into InAppCta.forAction and use the attributed add for "WISHLIST".
  • Bug Fixes

    • Read wishlist via IWishlistService/GetWishlist with access_token so private wishlists resolve; fall back when needed.
    • Auto-refresh the store access token via SteamService when missing or expired.

Written for commit 25a4a11. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added campaign-attributed wishlist additions from featured content.
    • Improved Steam wishlist access using authenticated sessions, automatic token refresh, and retry handling.
    • Added fallback support when attributed wishlist requests cannot be completed.
  • Bug Fixes

    • Improved handling of private, empty, and unauthenticated wishlist responses.
    • Preserved existing behavior for non-wishlist calls to action.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Wishlist attribution and authentication

Layer / File(s) Summary
Steam authentication handling
app/src/main/java/app/gamenative/service/SteamWishlistService.kt
The service extracts Steam IDs from JWTs and refreshes stored Steam tokens.
Attributed wishlist submission
app/src/main/java/app/gamenative/service/SteamWishlistService.kt, app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt
Wishlist CTAs pass campaign IDs. The service performs UTM-attributed store requests, retries after token refresh, and falls back to the CM API.
Authenticated wishlist checks
app/src/main/java/app/gamenative/service/SteamWishlistService.kt
Wishlist checks use stored tokens, refresh invalid tokens, and distinguish authenticated empty lists from private unauthenticated lists.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PR's main change: attributing featured wishlist actions through a UTM-tagged Steam Store visit.
Description check ✅ Passed The description includes the required sections, explains the change, identifies the change type, and provides checklist context, but no recording is attached.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wishlist-utm

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamWishlistService.kt (1)

65-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard 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. JSONObject then throws, and the exception unwinds to the catch in addToWishlistAttributed. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 78e9343 and 25a4a11.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/service/SteamWishlistService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/FeaturedCtaButton.kt

Comment on lines +59 to +62
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"

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Encode campaignId in the URL and fix the mature-content cookie name.

Two problems in this block:

  1. campaignId comes 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 makes Request.Builder().url(...) throw. The attributed add then silently falls back to the plain CM add, so attribution is lost.
  2. wantsmatureconctent is misspelled. Steam uses wants_mature_content. With the wrong cookie name, a mature-gated store page can return the age-check interstitial instead of the app page. The data-userinfo parse then fails and loggedIn stays 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.

Suggested change
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

@cubic-dev-ai cubic-dev-ai Bot 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.

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

Comment on lines +61 to +62
val utmUrl = "https://store.steampowered.com/app/$appId/" +
"?utm_source=gamenative&utm_medium=app&utm_campaign=$campaignId"

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.

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>
Suggested change
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")

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.

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>
Suggested change
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"

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.

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>
Suggested change
"birthtime=0; lastagecheckage=1-January-1970; wantsmatureconctent=1"
"birthtime=0; lastagecheckage=1-January-1970; wantsmaturecontent=1"

@utkarshdalal
utkarshdalal merged commit 45e59cd into master Aug 8, 2026
3 checks passed
antigravities added a commit to antigravities/GameNative that referenced this pull request Sep 4, 2026
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.

1 participant