
Short answer: Put two hostnames in /etc/hosts, embed the third-party origin in a first-party page, drive Safari with safaridriver, dump cookie state before and after a cross-site navigation, and write the result to a build-stamped JSON report. Run it on a clean hosted macOS so an existing Safari profile does not pollute the on-device ITP classifier. Figment's $29/month virtual Intel macOS runs Safari in its desktop environment and is enough for this automation; pick an M-series plan from $95/month when you need to pin several Safari/macOS pairs in parallel (exact chip, RAM and disk are confirmed in the quote; physical M-series collects requests until supply is confirmed). Start a configuration at /mac-hosting/start. Primary WebKit references: ITP 2.0 and Full Third-Party Cookie Blocking in Safari 13.1.
Safari's Intelligent Tracking Prevention is not one switch. Your evidence file needs to separate several behaviours, each triggered differently:
| Bucket | Trigger | Effect |
|---|---|---|
| Full third-party cookie block | Any cross-site context since Safari 13.1 | All third-party cookies blocked, no prior-visit exception |
| Immediate cookie partitioning for classified domains | Domain classified as a tracker (ITP 2.0) | Cookies partitioned in third-party contexts immediately |
| 7-day script-writable storage cap | LocalStorage/IndexedDB on classified domain | Non-cookie website data purged after 7 days |
| 30-day data purge | Classified domain, no user interaction for 30 days | Website data, including Storage Access grants, deleted |
| Storage Access required | Authenticated embed needs cross-site cookies | App must call document.requestStorageAccess() and get a user grant |
The anchor for most product tests is the full third-party cookie block: since Safari 13.1 third-party cookies are off by default with no previous-visit exception, as described in WebKit's Safari 13.1 announcement. The 7-day cap documented there applies to script-writable storage such as LocalStorage, not to every document.cookie write; treat cookie caps as observable rather than globally fixed and record the version you measured.
Two distinct registrable domains are needed to create a genuine cross-site context. Point two local names at loopback:
# /etc/hosts
127.0.0.1 first-party.test
127.0.0.1 third-party.test
Serve them on different ports:
# first-party origin
python3 -m http.server 8080 --directory ./sites/first-party
# third-party "tracker" origin
python3 -m http.server 8081 --directory ./sites/third-party
sites/first-party/index.html embeds a third-party subresource and sets a first-party cookie:
<script>
document.cookie = "fp_session=abc123; max-age=86400; SameSite=Lax";
</script>
<img src="http://third-party.test:8081/beacon.gif" />
<script src="http://third-party.test:8081/tag.js"></script>
sites/third-party/tag.js attempts a third-party cookie write:
document.cookie = "tp_track=xyz789; max-age=86400; SameSite=None; Secure";
console.log("third-party cookies:", document.cookie);
Enable ITP Debug Mode in Safari's Develop menu and keep Console.app open. The exact subsystem filter changes between Safari versions; toggle Debug Mode on and off while filtering in Console to confirm the current string on your target release. To push third-party.test into the classified bucket, load it as a subresource from at least three separate first-party pages before the measurement run, since the on-device classifier reacts to cross-site prevalence.
Use Safari's built-in WebDriver:
safaridriver --enable
Then run a short Python capture. It visits the third party as a first party to seed a cookie, visits the first party that embeds it, and records what is readable from where:
#!/usr/bin/env python3
import json, time
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.safari.options import Options
BUILD_ID = datetime.now().strftime("%Y%m%d-%H%M%S")
driver = webdriver.Safari(options=Options())
results = {"build_id": BUILD_ID, "domains": {}}
try:
driver.get("http://third-party.test:8081/")
time.sleep(1)
tp_fp_cookies = driver.get_cookies()
driver.get("http://first-party.test:8080/")
time.sleep(2)
tp_readable = driver.execute_script("return document.cookie;")
results["domains"]["third-party.test"] = {
"cookies_as_first_party": [c["name"] for c in tp_fp_cookies],
"visible_from_first_party_context": "tp_track" in tp_readable,
"raw_document_cookie_from_first_party": tp_readable,
}
fp_cookies_after = {c["name"]: c for c in driver.get_cookies()}
results["domains"]["first-party.test"] = {
"js_cookie_names": list(fp_cookies_after.keys()),
"max_remaining_days": max(
((c.get("expiry", 0) - time.time()) / 86400)
for c in fp_cookies_after.values()
) if fp_cookies_after else 0,
}
finally:
driver.quit()
out_path = f"itp-report-{BUILD_ID}.json"
with open(out_path, "w") as f:
json.dump(results, f, indent=2)
print(f"Report written: {out_path}")
This is an unexecuted example; run it against your own fixture and your installed Safari and treat the first captured JSON as the baseline, not a benchmark.
A healthy run looks like this:
{
"build_id": "20261110-034512",
"domains": {
"third-party.test": {
"cookies_as_first_party": ["tp_track"],
"visible_from_first_party_context": false,
"raw_document_cookie_from_first_party": ""
},
"first-party.test": {
"js_cookie_names": ["fp_session"],
"max_remaining_days": 1.0
}
}
}
visible_from_first_party_context: false with a cookie present under the same name when third-party.test is loaded directly is the signature of the full third-party cookie block. Store the file as a build artefact under its build ID so you can diff two consecutive reports whenever Safari updates or your fixture changes. Keep a separate run per Safari version you support rather than inferring behaviour across releases.
ITP changes land in Safari point releases, not macOS releases directly. Record the exact Safari build next to each JSON. For documented transitions and caps, cite the specific post, for example ITP 2.0 on cookie partitioning and the 30-day purge and the Safari release notes for per-version wording. Not every threshold is published; keep the on-device number you measured in the report rather than quoting a blog figure you did not reproduce.
A practical worksheet for a small matrix on Figment: one virtual Intel macOS instance at $29 for a stable Safari baseline, plus one M-series instance from $95 for current-generation Safari — entry list cost $124/month; annual at list is $1,488, before any contract terms quoted to you. Entry price is not equivalent hardware; the $29 instance is a virtualised Intel environment on Linux/KVM without GPU acceleration or audio, and the M-series chip, RAM and disk are confirmed in the quote. Use this only as a starting point to negotiate the actual plan you need.
A clean hosted Safari profile is the main reason to lift this test off a developer laptop: no stored classifier state, no extensions, no history. The $29 virtual Intel macOS instance runs Safari in its desktop environment and reaches it through the browser console access path, which is enough for safaridriver-driven capture against a loopback fixture. Pick M-series from $95 when you need a current Safari build tied to Apple-silicon-only features or want two Safari versions running side by side on separate instances. Request a configuration quote at /mac-hosting or begin at /mac-hosting/start.
Related guides: