cdpfleet Docs GitHub Dashboard

Cases / #2 · 2026-09-30 · Medium

Emulating an iPhone on real Chrome: what changes, and what gives you away

Playwright device descriptors fix the viewport, touch and user agent — and leave five tells a site can read.

Google Chrome

Run on production on 2026-09-30: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

You need the mobile version of a site, so you open a context with Playwright's iPhone 15 Pro or Pixel 7 descriptor. The page renders the mobile layout. But is the browser now convincing as a phone? A bot check can look at the network (TLS, client hints) and at JavaScript APIs the descriptor doesn't touch. Let's compare a desktop context, an iPhone context and a Pixel context in the same Chrome session.

What we used, and why

WhatWhy
chromeReal Google Chrome, the most common desktop browser. One session serves all three contexts.
devices['iPhone 15 Pro'], devices['Pixel 7']Playwright's descriptors: user agent, viewport, screen, device scale factor, isMobile, hasTouch. (Playwright for Java has no registry, so the Java version spells the same values out.)
tls.peet.wsShows what the network sees: the user agent, client-hint headers and TLS fingerprint.
page.evaluateReads what page scripts see: viewport, touch points, pointer type, navigator.platform, navigator.userAgentData.

How it works

  1. Launch one Chrome session.
  2. Open three contexts: plain, iPhone 15 Pro, Pixel 7.
  3. In each, load tls.peet.ws and read the JavaScript-visible device signals.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium, devices } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

const res = await fetch('https://starter.cdpfleet.com/chrome/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

// What a page can see about the device, plus what the network sees (tls.peet.ws).
async function inspect(context) {
  const page = await context.newPage();
  const fp = await (await page.goto('https://tls.peet.ws/api/all', { timeout: 60000 })).json();
  const js = await page.evaluate(() => ({
    viewport: `${innerWidth}x${innerHeight}`,
    screen: `${screen.width}x${screen.height}`,
    device_pixel_ratio: devicePixelRatio,
    max_touch_points: navigator.maxTouchPoints,
    coarse_pointer: matchMedia('(pointer: coarse)').matches,
    platform: navigator.platform,
    ua_data_mobile: navigator.userAgentData?.mobile ?? null,
    ua_data_platform: navigator.userAgentData?.platform ?? null,
  }));
  const headers = fp.http2.sent_frames.find((f) => f.frame_type === 'HEADERS').headers;
  const header = (name) => headers.find((h) => h.startsWith(`${name}: `))?.slice(name.length + 2) ?? null;
  await page.close();
  return {
    user_agent: fp.user_agent,
    ...js,
    sec_ch_ua_mobile: header('sec-ch-ua-mobile'),
    sec_ch_ua_platform: header('sec-ch-ua-platform'),
    ja4: fp.tls.ja4,
    akamai_h2_hash: fp.http2.akamai_fingerprint_hash,
  };
}

try {
  const desktop = await browser.newContext();
  const iphone = await browser.newContext({ ...devices['iPhone 15 Pro'] });
  const pixel = await browser.newContext({ ...devices['Pixel 7'] });
  console.log(JSON.stringify({
    desktop: await inspect(desktop),
    'iPhone 15 Pro': await inspect(iphone),
    'Pixel 7': await inspect(pixel),
  }, null, 2));
} finally {
  await browser.close();
}

What we got

ContextUser agentViewportDPRTouchnavigator.platformuserAgentDataJA4
desktopMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/203.0.113.1 Safari/537.361280x72010Linux x86_64Linuxt13d1517h2_8daaf6152771_cb7bf5808d99
iPhone 15 ProMozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Mobile/15E148 Safari/604.1980x164431Linux x86_64iOSt13d1517h2_8daaf6152771_cb7bf5808d99
Pixel 7Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36981x19962.6251Linux x86_64Androidt13d1517h2_8daaf6152771_cb7bf5808d99

From the Node.js run on 2026-09-30. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
{
  "desktop": {
    "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/203.0.113.1 Safari/537.36",
    "viewport": "1280x720",
    "screen": "1280x720",
    "device_pixel_ratio": 1,
    "max_touch_points": 0,
    "coarse_pointer": false,
    "platform": "Linux x86_64",
    "ua_data_mobile": false,
    "ua_data_platform": "Linux",
    "sec_ch_ua_mobile": "?0",
    "sec_ch_ua_platform": "\"Linux\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  "iPhone 15 Pro": {
    "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.4 Mobile/15E148 Safari/604.1",
    "viewport": "980x1644",
    "screen": "393x852",
    "device_pixel_ratio": 3,
    "max_touch_points": 1,
    "coarse_pointer": true,
    "platform": "Linux x86_64",
    "ua_data_mobile": true,
    "ua_data_platform": "iOS",
    "sec_ch_ua_mobile": "?1",
    "sec_ch_ua_platform": "\"iOS\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  },
  "Pixel 7": {
    "user_agent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36",
    "viewport": "981x1996",
    "screen": "412x915",
    "device_pixel_ratio": 2.625,
    "max_touch_points": 1,
    "coarse_pointer": true,
    "platform": "Linux x86_64",
    "ua_data_mobile": true,
    "ua_data_platform": "Android",
    "sec_ch_ua_mobile": "?1",
    "sec_ch_ua_platform": "\"Android\"",
    "ja4": "t13d1517h2_8daaf6152771_cb7bf5808d99",
    "akamai_h2_hash": "52d84b11737d980aef856699f885ca86"
  }
}

Takeaways