Skip to content
All articles
8 min read

Deep Link Opened the App, Then Returned to the Browser

A phone screen bouncing from a browser to an app icon and back to the browser

You watch the click fire, the intent resolve, Android hand off to the target app — and the visitor is back on your landing page a second later, as if the link had opened the browser instead of the app to begin with. Your log says the app opened. The client wants to know why the visitor didn’t do anything once they were supposedly in it. Both of you are right, and that’s the problem: a successful jump and a visitor who ends up back in the browser anyway are not mutually exclusive.

The same mechanism, two outcomes, one device

This isn’t a hypothesis. It’s what happened on a single Pixel 8, Android 15, Chrome 131, running the same dispatch mechanism twice against two different apps:

InstagramTikTok
Dispatchintent://www.instagram.com/instagram/#Intent;scheme=https;package=com.instagram.android;endSame intent:// mechanism, TikTok’s package
Handler activitycom.instagram.url.UrlHandlerActivitycom.ss.android.ugc.aweme.deeplink.AppLinkHandlerV2
System logTask created, app stayed foregroundTask created, then closed with numActivities=0
What the visitor sawInstagram, on the destination profileThe browser, on the original web page
WhyThe app had a session to render the destination withThe app was logged out and had nothing to render

Instagram received the link, opened, and stayed on the destination profile. TikTok received the same class of link, at its own handler, and closed itself immediately — the system log shows the task being created with the deep link on it, then closing with numActivities=0, and the visitor landing back in the browser on the exact page they left. Both apps were handed the link correctly. Only one of them had something to show for it.

numActivities=0 is Android’s own accounting of the task it just created: the app opened a task to hold the deep link’s destination screen, and that task now contains zero activities, because the app tore it down instead of populating it. That’s not a crash and it isn’t logged as an error anywhere a campaign report would see it. It’s the app making a deliberate choice about what to do with a link it can’t render, and choosing “nothing” over an error screen or a login wall.

This isn’t a defect specific to TikTok, and the point of putting these two runs side by side isn’t to single one app out. It’s that the same failure mode is available to any app on the other end of a deep link, and which one you happen to observe depends on the account state of the visitor holding the phone, not on which app you targeted or how the link was built. A logged-in TikTok account, handed the identical intent at the identical handler, has a session to render the destination with — the same condition that made Instagram’s run succeed.

What “opened” actually measures

The routing layer’s job ends at handoff: build the right intent://, resolve the right package, and get Android to launch the target app. That’s what a click log or an app-open funnel is built to see, and by that measure, both runs above succeeded — Android launched the app in both cases. What the routing layer cannot see, and never could, is what the app does with the link once it has it. Instagram had an active session and rendered the destination. TikTok didn’t, and rather than showing an error or a login wall, it closed its own handler and let the visitor fall back to wherever they’d come from.

This is why “did the app open” is the wrong question to build a report around. It’s a real question with a real answer, and the answer is frequently yes for a visitor who never saw anything inside the app. An app-open funnel that stops at handoff will count the TikTok run in this comparison exactly the same way it counts the Instagram run — as a success — because from the routing layer’s vantage point, that’s exactly what it was.

Two different things are true at once, and a single metric can’t hold both:

What it measuresWhere it’s decided
HandoffAndroid launched the correct app at the correct handlerThe routing layer, at the moment the intent resolves
In-app outcomeWhether the app rendered the destination or bounced the visitor backThe app itself, based on its own session state at launch

A campaign dashboard that only has room for one of these rows will default to handoff, because handoff is the easy half to instrument — it’s a click-side event with a clear success signal, the same kind of easy-to-capture signal a pixel fires regardless of what happens after it. The in-app outcome has no equivalent signal on the sending side at all; it only exists on the device, inside the app, after control has already passed.

Nothing on the sending side can prevent this

There’s no fallback URL, no retry, no better-constructed intent that changes what happened here. The routing layer did its job: it got the right app, at the right handler, with the link attached. What the app did next was decided by state the routing layer has no access to and no way to query in advance — whether that TikTok account had a valid session at the moment the link arrived. A visitor who is logged in gets a different outcome than a visitor who isn’t, from the identical link, dispatched the identical way. That’s not a routing failure to fix. It’s a condition the routing layer was never in a position to detect.

How to tell the two cases apart when you’re diagnosing

From the click side, an Instagram-style success and a TikTok-style bounce look identical up to the moment of handoff — same intent dispatched, same package resolved, same app launched. The difference only shows up after that point, and the only place it’s actually visible is the same place these two runs were told apart in the first place: the device itself, mid-click, with the system log open. Reproducing the click on a real device and watching what Android reports — which handler activity received the link, whether the resulting task stays foreground or closes with numActivities=0 — tells you definitively whether the app held the visitor or handed them back. Reproducing the same click logged into the destination app versus logged out is the fastest way to see whether a given app’s bounce-back is session-dependent, the way TikTok’s was here.

What you can’t do is infer the difference from anything available at the link-construction stage. The intent:// you build, the package you target, the fallback rules you set — none of it carries information about what the app will do once it has control, because that information doesn’t exist until the app evaluates its own session at launch time.

A practical version of this check, once you suspect a given app is bouncing visitors back:

  1. Reproduce the click on a real device with the destination app installed and logged in. Confirm the handler activity and note whether the resulting task stays foreground.
  2. Log out of the same app and reproduce the identical click. If the outcome changes — the app now closes its handler and returns you to the browser — the bounce is session-dependent, not a broken link.
  3. Compare the two handler activities the OS reports. A visitor-facing symptom that reads the same from your side (“app opened, visitor didn’t convert”) can come from two different components on the app’s side, and only the device tells you which one you’re looking at.

None of these three steps touch the link itself, because the link was never the variable. The variable is what’s logged into the phone running it.

What this means for what you promise a client

“App opened” and “visitor converted in-app” are two different claims, and a report that collapses them into one number is making a promise the routing layer never had the ability to keep. The honest version of a routing report says the link was handed off to the correct app successfully — that’s the thing actually happening at, and measurable from, the routing layer. Whether the visitor stayed there afterward is a separate outcome, determined by the destination app’s own session state, and no amount of tuning on the sending side moves that number.

Concretely: if a client asks why an app-open campaign isn’t converting the way the open rate suggests it should — the same shape of question as a conversion that never arrived — the answer might not be in the campaign at all. It might be that a meaningful share of visitors are opening the target app while logged out, and the app itself is declining to show them anything — which the routing layer executed correctly, and which nothing about targeting, creative, or link construction can change. Reporting handoff success and in-app outcome as two separate lines, instead of one blended “app opened” figure, is the difference between a client who understands what they’re actually seeing and one who assumes every open reached its destination.

This is the split DarkCore PWA Tracker keeps at the routing layer: a recorded handoff to the target app, distinct from whatever the visitor does after control passes to it — because the two are decided by different systems, and reporting them as one number answers a question nobody asked.

  • deep link opens app then goes back to browser
  • app link returns to browser
  • deep link not working android
  • applinkhandler