Skip to content

fix(redirect): send App Store and Google Play URLs exactly as configured - #68

Merged
onamfc merged 1 commit into
mainfrom
fix/store-url-not-decorated
Oct 8, 2026
Merged

onamfc merged 1 commit into
mainfrom
fix/store-url-not-decorated

Conversation

@onamfc

@onamfc onamfc commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Description

Before sending a visitor on, the redirect decorates the destination: it adds UTM tags, each deep-link parameter as a query parameter, and the opt-in lf_click. That's right for a website, but it was also applied to the store URL chosen when a phone doesn't have the app.

Google Play identifies the app by its own ?id=. A link with a deep-link parameter named id, which is a common name, replaced the app's id:

configured:  https://play.google.com/store/apps/details?id=com.example.app
sent:        https://play.google.com/store/apps/details?id=evt_42&route=EVENT_VIEW&utm_source=…

Play answers that with a 404. In the Play app the visitor sees "Error retrieving information from server" instead of the listing they need to install from. App Store URLs carry the app id in the path, so they kept working, but they gained parameters nothing reads.

Store URLs are now sent exactly as configured. Nothing is lost: neither store passes query parameters on to the app, and after install the deep-link data still reaches the app through deferred deep linking. Websites are decorated exactly as before.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • ♻️ Code refactoring
  • ⚡ Performance improvement
  • ✅ Test addition or update

Related Issues

None filed publicly; found from a user report of the Play Store error.

Changes Made

  • isAppStoreUrl() (exported from src/routes/redirect.ts) recognises play.google.com, apps.apple.com and itunes.apple.com.
  • decorateWebDestination() returns store URLs unchanged. That covers the mobile store redirect and any path where a store URL is used as a web fallback.
  • New src/routes/redirect.store-url.test.ts.

Testing

  • Tests pass locally (npm test): 410 passed, 2 skipped
  • New tests added for new functionality
  • Existing tests updated (if needed): none needed; all existing redirect tests pass unchanged
  • Type checking passes (npm run typecheck)

Test Coverage

redirect.store-url.test.ts (8 tests, through the real route with fastify.inject()):

  • Android Chrome → the Play URL exactly as configured, despite id/route parameters, UTM and append_click_id
  • iOS Safari → the App Store URL exactly as configured
  • a store URL used as the web fallback isn't decorated
  • desktop → the web fallback still gets UTM, deep-link parameters and the click id
  • app-open guard: an Android in-app browser still goes to the web fallback, with its parameters
  • app-open guard: an App Link still wins over every fallback
  • isAppStoreUrl: case-insensitive, and rejects look-alike hosts (play.google.com.evil.example), store URLs inside query strings, and garbage

5 of the 8 fail without the fix. The other 3 are the website and app-open guards, which describe behaviour that must not change.

npm test -- src/routes/redirect.store-url.test.ts

Screenshots/Examples

isAppStoreUrl('https://play.google.com/store/apps/details?id=com.example.app'); // true
isAppStoreUrl('https://play.google.com.evil.example/x');                         // false

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (n/a: no documented behaviour changes)
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have used conventional commit messages (e.g., feat:, fix:)
  • I have signed the Contributor License Agreement (the CLA bot will prompt you)

Breaking Changes

None. Store URLs no longer carry UTM tags or deep-link parameters; neither store reads them, and websites are unaffected.

Additional Notes

App-open is unchanged. This only changes the URL string of a store destination after the route has been chosen. Universal Link / App Link redirects still take priority, and in-app browsers (Gmail, Instagram, Facebook, WebViews) are still sent to the web fallback first, where a Universal Link / App Link gets a second chance to open an installed app. The existing pickMobileFallbackUrl in-app browser tests pass unchanged, and two of the new tests assert this at route level.

The redirect decorated every http(s) destination like a website: UTM tags,
each deep-link parameter as a query parameter, and the opt-in click id. That
included the store URL chosen when a phone does not have the app.

Google Play identifies the app by its own ?id=, so a link with a deep-link
parameter named id replaced the app's id and Play answered 404 — the visitor
saw "Error retrieving information from server" instead of the listing they
needed to install from. App Store URLs carry the app id in the path and were
unaffected, but gained parameters nothing reads.

Store URLs (play.google.com, apps.apple.com, itunes.apple.com) are now left
untouched. Nothing is lost: neither store passes query parameters to the app,
and after install the deep-link data still arrives through deferred deep
linking. Websites are decorated exactly as before.

App-open routing is unchanged: Universal Link / App Link redirects still win,
and in-app browsers are still sent to the web fallback first. The new tests
assert both alongside the fix.
@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@onamfc
onamfc merged commit 8d9db4b into main Oct 8, 2026
14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant