mobilegame-qadeep-linksattributiontesting

Testing deep links: deep links, Universal Links, App Links and the state matrix

Deep links are what ads, referrals, sharing and push rely on: a link must open the right screen inside the app, not the home screen and not the browser. Sounds simple, but this is exactly where the state matrix hides — the reason for “sometimes it works, sometimes it doesn’t”. Let’s look at how to test deep links properly.

  • Custom scheme (myapp://route): simple, but if the app isn’t installed, the link leads nowhere (error/blank). Not viable as the only mechanism.
  • iOS Universal Links (a regular https://): open the app if installed, otherwise the site. Require an apple-app-site-association (AASA) file on the domain.
  • Android App Links (also https://): the Android equivalent, require assetlinks.json and verification. Without verification the system shows a “chooser” dialog or opens the browser.

The state matrix — the main source of bugs

The same deep link must work correctly in all app states:

  • App running (foreground) → navigate to the right screen without a reload.
  • In the background → resume and navigate to the right place.
  • Killed / cold start → launch, and after initialization — navigate (a common bug: the deep link is lost, the home screen opens).
  • Not installed → store or web fallback (see deferred below).

The minimum test set for each deep link = 4 states × (valid / broken link). Most bugs are exactly in killed + cold start.

Deferred deep linking

The scenario: a user clicked a link, the app isn’t there → went to the store → after install must land on the screen the link pointed to (a product, promo, group invite). That’s deferred deep linking — navigation “arrives” after install. Verify that:

  • The target screen opens on the first launch after installing from the link.
  • Attribution is correct: the tracker (Adjust/AppsFlyer) understood which campaign/referral drove it — what deferred deep linking is.
  • If the data didn’t arrive — a sensible default screen, not a blank or a crash.
  • No app → open the store (the right one for the platform) or the web version of the page.
  • A stale/broken link (product deleted, promo ended) → a clear screen (“not found”/section home), not a white screen or a crash.
  • An unknown path in the link → graceful handling, default routing.

A deep link is user input controlled by anyone. Verify:

  • Parameters from the link don’t lead to private screens bypassing authorization (opening myapp://account/settings without login).
  • No open redirect: ?next=https://evil.com doesn’t take the user/token outside.
  • Injections in parameters (id, search) are escaped, don’t break the screen and don’t leak into queries.
  • Sensitive actions (payment, changing data) aren’t executed “by link” without confirmation.

Domain verification — why it “opens in the browser”

The classic complaint: “the Universal Link opens the site, not the app.” The cause is almost always the domain association file:

  • iOS: apple-app-site-association (AASA) available at https://domain/.well-known/apple-app-site-association, served as JSON, no redirects, with the correct appID. See Associated Domains and Universal Links.
  • Android: assetlinks.json in /.well-known/, App Links verification passed (verify Android App Links, App Links).
  • Important: a Universal Link must be tapped (from Notes/a messenger), not pasted into the Safari address bar — there it opens as a regular site. That’s not a bug, it’s system behavior.

Attribution and analytics

Marketing pays for clicks, so campaign parameters (utm/referral/ad id) must reach the tracker exactly once and bind to the right source. Verify: duplicate events, parameter loss on cold start, correctness in the deferred scenario.

Tools

  • Android: adb shell am start -W -a android.intent.action.VIEW -d "<url>" <package> — open a deep link on a device/emulator.
  • iOS simulator: xcrun simctl openurl booted "<url>".
  • Checking AASA/assetlinks and redirects — via HTTP interception (Proxyman/Charles): what the domain actually serves.
  • A real tap: a link in Notes/chat (not the address bar), on a clean device for deferred.
  1. Each deep link is tested in 4 states: foreground / background / killed / not installed.
  2. Killed + cold start leads to the right screen (not the home screen).
  3. Deferred: after installing from the link, the target screen opens.
  4. Campaign/referral attribution is correct and without duplicates.
  5. Fallback when the app is missing → store/web.
  6. A broken/stale link → a sensible screen, not a crash.
  7. The custom scheme isn’t the only mechanism (there are UL/App Links).
  8. AASA available, JSON, no redirect, correct appID (iOS).
  9. assetlinks.json in place, App Links verified (Android).
  10. The link doesn’t open private screens bypassing authorization.
  11. No open redirect and no injections via parameters.
  12. Tested on a real tap (Notes/chat), not just from the address bar.

Bottom line

A deep link seems like “just a link,” but its quality is the state matrix × fallbacks × domain verification × security × attribution. Most bugs live in killed + cold start and in deferred, while “it opens in the browser” is almost always an unverified AASA/assetlinks. A systematic state-based checklist catches this before an ad campaign breaks.

Sources