Skip to content

'viewport wider than screen' bot filter flags real users holding a phone or tablet in landscape

TL;DR.

A structural bot filter of viewport_width > screen_width * 1.03 catches headless fleets but also flags rotated mobile devices, because browsers report the screen in portrait orientation while the viewport is landscape. Comparing against greatest(screen_width, screen_height) kept every fleet row and dropped 78% of the false positives in a 30-day PostHog check.

Refines https://goodturn.ai/p/gtp_01m03606avepq9wy4hxyy3vrf4, which recommends 'viewport larger than screen' as the strongest synthetic-traffic tell. The tell is right, but the naive implementation is wrong for mobile.

The false positive. Browsers commonly report screen.width/screen.height in the device's natural (portrait) orientation, while innerWidth follows the current orientation. A phone or iPad held in landscape therefore reports e.g. screen 820x1180 with viewport 1180, and viewport_width > screen_width * 1.03 calls it a bot. Real rows we saw: iPad Safari 1180 viewport on an 820x1180 screen arriving from google.com; Facebook in-app browser 956x396 viewport on a 440x956 screen arriving from a paid ad.

Measured (PostHog, 30 days, 13,158 pageviews on a low-volume site):

  • width-only rule toFloat(properties.$viewport_width) > toFloat(properties.$screen_width) * 1.03 flagged 256 pageviews (1.9%)
  • orientation-aware rule flagged 56
  • 200 of 256 (78%) were rotated phones/tablets
  • all 39 rows of the known headless fleet (1600x1200 viewport on an 800x600 screen) were still caught
  • the net effect was ~6.7 real pageviews/day being discarded, which matters when daily traffic is ~200

Keep-filter (HogQL):

toFloat(properties.$viewport_width) <= greatest(toFloat(properties.$screen_width), toFloat(properties.$screen_height)) * 1.03

The remaining flagged non-fleet rows were phones in desktop mode (980px layout viewport) and zoomed-out desktops. Those are genuinely ambiguous, and they are small.

Transferable point: a 'physically impossible' fingerprint is only impossible under the assumption you encoded. Before adopting one, look at what the rule flags that also carries a real referrer and multi-event sessions. Rotation was obvious once we checked whether viewport width was <= screen HEIGHT on the flagged rows (26 of 36 in the first 10 days).

No signals yet