DEV Community

Vladimir Elchinov for Session Replay

Posted on

Every Browser on an iPhone Is Safari, and the Bug Report Will Not Say Which One

Safari 27 shipped on Monday with iOS 27. By the weekend most iPhones will be on it, and most of their owners will not know. That is the ordinary story of a browser release. The less ordinary part is how many "browsers" that one update just changed, and how badly a bug report describes any of them.

One engine, many names

Apple's App Store Review Guidelines, 2.5.6: "Apps that browse the web must use the appropriate WebKit framework and WebKit JavaScript." An entitlement for an alternative engine exists for the EU and Japan; outside a handful of apps in those markets, the rule is the rule. So:

  • Chrome on iPhone is WebKit with Chrome's toolbar. Same for Firefox, Edge, Brave, Opera.
  • The browser inside Instagram, Facebook, Slack, Gmail, TikTok and LinkedIn is WKWebView, a system component. It is WebKit with whatever settings the app chose.
  • "Open in Safari" from those apps is the actual Safari.

All of those updated overnight with iOS 27, because WebKit is part of the OS, not part of the app. The Instagram in-app view that renders your page got scroll anchoring and eight Content Security Policy fixes on Monday, and Instagram shipped nothing.

When a report says "Chrome on my iPhone", the engine is Safari's, and reproducing it in Chrome on your desktop reproduces the wrong one. When it says "I opened it from Instagram", the engine is Safari's too, but the feature set is not: no extensions, no address bar, the app's own cookie jar, and often no way to get to the tab in any other browser.

What the user agent actually tells you

The reporter cannot tell you which of those they were in. The user agent string can, if you know its shape. Three tokens do most of the work:

export function iosSurface(ua = navigator.userAgent) {
  if (!/iPhone|iPad|iPod/.test(ua) && !isIpadPretendingToBeAMac()) return null;

  if (/CriOS\//.test(ua))  return "chrome-ios";     // still WebKit
  if (/FxiOS\//.test(ua))  return "firefox-ios";    // still WebKit
  if (/EdgiOS\//.test(ua)) return "edge-ios";       // still WebKit

  // Safari names itself twice: "Version/27.0 ... Safari/604.1".
  // A WKWebView sends neither token. That absence is the whole signal.
  if (/Version\/[\d.]+.*Safari\//.test(ua)) return "safari";
  return "in-app-webview";
}

// Since iPadOS 13, Safari on an iPad asks for desktop sites by default and
// says it is a Mac. The touch points give it away.
function isIpadPretendingToBeAMac() {
  return navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
}
Enter fullscreen mode Exit fullscreen mode

Two of those lines are the ones that cost people afternoons.

The missing Safari/ token. A WKWebView user agent reads something like Mozilla/5.0 (iPhone; CPU iPhone OS 27_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148. No Version/, no Safari/. Most user agent parsers, handed that, print "Safari" anyway, because WebKit on iPhone is Safari as far as they are concerned. So your analytics say Safari, your error tracker says Safari, and the failure only happens inside the Instagram browser, where the app has disabled something you rely on. Apps do append their own markers (Instagram, FBAN/FBIOS, LinkedInApp), but not all of them, and not consistently; the absent tokens are the reliable test.

The iPad that says it is a Mac. A report of "Safari on Mac, buttons do not respond" can be an iPad. It sends a Macintosh; Intel Mac OS X user agent, gets your desktop layout at 1024 points wide, and then tries to tap hover menus with a finger. Every hover-only interaction on the page is broken for that user, and the report will say Mac. maxTouchPoints is the only honest witness.

One more thing the string will never tell you: navigator.userAgentData, the structured replacement, is undefined on every one of the surfaces above. WebKit has not shipped it. Code that feature-detects the new API and treats its absence as "an old browser" has just classified every iPhone on earth as old.

Why this is a reporting problem before it is a detection problem

The function above is useful in your own telemetry. It is useless in the moment that matters, which is when a person types "does not work on my phone" into a form and hits send.

That person cannot see the user agent. They do not know that the Chrome icon they tapped opens Safari's engine, or that the link they followed from a Slack message never left Slack. They will answer "Chrome" or "Safari" or "my iPhone" in good faith, and every one of those answers points the developer at the wrong reproduction.

So the report has to carry the string itself, captured from the page at the moment of the failure, along with the viewport and the touch points. Not because the reporter should have known, but because they could not have. Anything that asks them to describe their browser is asking a question they are structurally unable to answer correctly on iOS, and this week, with one engine update landing on every surface at once, the wrong answer costs a reproduction that will never succeed.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.