Browsers · WhiskerJS 0.1.0

Safari on macOS

Safari offers some signals no other browser has and lacks the Chromium-only ones; stock Safari has no exclusions, and with Advanced Fingerprinting Protection five components are left out.

In plain words

Safari on the Mac uses Apple’s WebKit engine. It lacks a few measurements that only Chromium-based browsers offer, such as the device memory and Client Hints, so those come back as “unavailable”. In return it has a few of its own, such as Apple Pay support and Private Click Measurement, and WhiskerJS reads the audio output latency here. Nothing is left out in a normal window. When Safari’s Advanced Fingerprinting Protection adds noise to drawings and sound (always in private browsing), WhiskerJS notices with tiny test reads and leaves those signals out.

At a glance

  • Browser class: safari (the vendor is Apple Computer, Inc.); safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback.
  • Excluded components: none in stock Safari; 5 under Advanced Fingerprinting Protection (audio, canvas, screen_frame, screen_resolution, webgl_image, ruleId safari_afp).
  • CPU threads: a 4 or an 8 is reported as the bucket 4_8.
  • Unavailable by design: device_memory, heap_limit, ua_client_hints, ua_versions, keyboard_layout.
  • Read only here and on iOS (and, for latency, Android): apple_pay with a version, private_click, audio_latency.
  • Lab sample: Mac Safari kept one id across 2 runs at signalsVersion 1.

What works

  • Safari-specific components: apple_pay reports version=<n>;can_pay=<1 or 0>, found by asking supportsVersion() from 20 down; can_pay is blocked where canMakePayments() throws (insecure or cross-origin contexts). private_click reads attributionSourceId from a new, unattached <a> element. audio_latency reads AudioContext.baseLatency and closes the context at once.
  • Graphics: canvas and WebGL are read as elsewhere. The webgl_renderer string is often generic (for example Apple GPU), which tells less than on other browsers.
  • hardware_concurrency: read 16 times; a 4 or an 8 is reported as 4_8, because WebKit alternates between 4 and 8 for scripts on its known-fingerprinting list (Safari 26, Safari Technology Preview 254), also between page loads. Other values are exact.
  • Fonts, font preferences, offline audio, Math, codecs, plugins, locale, storage presence and the display preference queries.

What is unavailable or blocked

These report unavailable with ruleId no_api because Safari does not offer the API:

Component Missing API
device_memory navigator.deviceMemory
heap_limit performance.memory
ua_client_hints navigator.userAgentData
ua_versions navigator.userAgentData
keyboard_layout navigator.keyboard

Because a non-ok status counts the same in every run, these do not make the id unstable: they are simply “missing” every time. webgpu depends on the Safari and macOS version; it is unavailable where navigator.gpu is missing.

What WhiskerJS excludes and why

Nothing in stock Safari. The safari row of the exclusion table is empty.

Safari’s Advanced Fingerprinting Protection (AFP) is on in private browsing, and in all browsing where the Safari setting (or a newer Safari’s default) says so. It adds noise to canvas, WebGL and Web Audio readback and reports the window size as the screen. WhiskerJS does not read the setting: it classes a Safari as safari_afp when the canvas or WebGL known-answer probe came back altered. Then:

Component Why it is left out
audio Web Audio readback gets noise
canvas canvas readback gets noise
screen_frame the window size is reported as the screen
screen_resolution the window size is reported as the screen
webgl_image WebGL readback gets noise

All five report excluded with ruleId safari_afp. Stock Safari reads the known answers back exactly and stays safari. Wherever noise is not covered by a row, canvas, webgl_image and audio still catch it: two renders that differ give noisy / render_mismatch, and a failed known-answer probe gives noisy / seeded_noise. canvas_readback then reports altered.

Integrity flags to expect

  • iframe_mismatch used to fire in stock Safari and in Playwright’s WebKit build, because WebKit gives a hidden (display: none) iframe a 0 by 0 screen. Since signalsVersion 9 the screen values are compared only when the iframe reports a screen wider than 0, so this no longer happens.
  • engine_mismatch checks that a safari or safari_afp class behaves like WebKit: the printed source of eval must be 37 characters and navigator.productSub 20030107. A user-agent switcher can raise it.
  • native_tampering and prototype_lie with extensions that change browser functions.
  • vm_renderer when macOS runs as a virtual machine guest (Apple Paravirtual device). ua_ch_mismatch is never checked: Safari has no navigator.userAgentData. media_query_mismatch does not run for the safari_afp class.
  • Advanced Fingerprinting Protection’s noise is never a flag: it shows in noise_profile and the excluded components.

Early lab observations

An early, small sample at signalsVersion 1, not a benchmark: on a Mac, Safari kept one id across 2 runs, different from the ids of the other three pairs. device_memory was unavailable, by design. The sample used the first collector (30 components), not the current 47.

Testing it yourself

Run the live demo in a normal Safari window and then in a private window, and compare browser_class, noise_profile and the canvas, WebGL and audio statuses. In the private window the class should say safari_afp and five components excluded, which is why the visitorId differs between the two modes. The lab mode copies one line per run. Nothing is sent anywhere.

Support table for Safari on macOS

Expected status of every component in this browser at signalsVersion 9, from the support matrix (updated 2026-10-26). Basis "code" means expected from the library code and documentation, not yet measured; "lab" means seen in the early lab sample.

ComponentStatusBasisNote
apple_payokcodeversion=<n>;can_pay=<1, 0 or blocked>; can_pay is blocked in insecure or cross-origin contexts.
audiovariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
audio_latencyokcodeRead for browser class safari; the context is closed at once.
browser_classokcodeClass safari; safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback.
canvasvariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
canvas_readbackvariescodeexact, or altered with Advanced Fingerprinting Protection (always in private browsing).
color_depthokcode
color_gamutokcode
content_blockersokcodenone, or the bait names a content blocker hid.
cpu_architectureokcode
device_memoryunavailablelabSeen unavailable on Mac Safari in the lab sample; Safari does not offer navigator.deviceMemory.
dynamic_rangeokcode
font_preferencesokcode
fontsokcode
fonts_canvasokcode
forced_colorsokcode
hardware_concurrencyokcodeRead 16 times; a 4 or an 8 is reported as the bucket 4_8, because WebKit alternates between them for scripts on its known-fingerprinting list.
heap_limitunavailablecodeperformance.memory is a Chromium API. Reports unavailable / no_api.
intl_localeokcode
keyboard_layoutunavailablecodeNo navigator.keyboard here: unavailable / no_api.
languagesokcode
mathokcode
media_codecsokcode
noise_profileokcodeexact in a normal window; seeded for the APIs Advanced Fingerprinting Protection randomises.
pdf_viewerokcode
permissionsokcode
pixel_ratiookcode
platformokcode
pluginsokcode
prefers_contrastokcode
private_clickokcodeSafari Private Click Measurement exposes attributionSourceId.
reduced_motionokcode
screen_framevariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
screen_resolutionvariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
speech_voicesokcodeDigest of the voice list, or none; some browsers fill the list late.
storageokcode
time_zoneokcode
touch_supportokcode
ua_client_hintsunavailablecodenavigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api.
ua_versionsunavailablecodenavigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api.
vendorokcode
vendor_flavorsokcode
webgl_imagevariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
webgl_paramsokcodeWhere WebGL 1 is available; unavailable / no_context without it.
webgl_rendererokcodeOften a generic string such as Apple GPU.
webgl_vendorokcode
webgpuvariescodeDepends on browser version, operating system and GPU; needs a secure context. unavailable / no_api without navigator.gpu, no_context without an adapter.

Questions and answers

Why is device_memory unavailable in Safari?

Safari does not offer navigator.deviceMemory; only Chromium-based browsers do. WhiskerJS reports it as unavailable with ruleId no_api, by design. The early lab sample showed exactly this on Mac Safari.

Does Safari's private browsing change the id?

Yes, once. Private browsing turns on Advanced Fingerprinting Protection, which adds noise to canvas, WebGL and Web Audio readback and reports the window size as the screen. WhiskerJS's known-answer probes notice the noise, class the browser safari_afp and leave audio, canvas, screen_frame, screen_resolution and webgl_image out. The id then differs from the normal window's id, but stays the same from one private session to the next.

Why does hardware_concurrency say 4_8?

WebKit gives scripts on its known-fingerprinting list a core count that alternates between 4 and 8, also between page loads. WhiskerJS reports a WebKit 4 or 8 as the one bucket 4_8 so the id does not flap. Other values are reported exactly.

Does WhiskerJS show an Apple Pay sheet?

No. It only checks whether ApplePaySession exists, the highest supported version and canMakePayments(). No payment sheet is shown and no request is made.