Skip to main content
Technical Tracking

How to Audit a "Cookieless" Analytics Tool in DevTools

By , MarTech & Analytics Engineer

Published Updated

Quick answer: “Cookieless” is a marketing term that vendors define differently. Some tools genuinely skip all browser storage; others store tracking state in localStorage, IndexedDB or via Set-Cookie response headers from a server. A third category writes nothing but reads device parameters — screen dimensions, canvas fingerprint, CPU count — to derive a session hash without cookies. Four DevTools checks, one of which needs a console snippet, show you which category your tool is in. Whether any of those approaches needs a consent banner is a legal question covered in the consent rules post.

Check 1: Does the tool write anything to the browser?

Open DevTools (F12 → Application tab) and look under the Storage section on the left: Cookies, Local Storage, Session Storage, IndexedDB, Cache Storage.

  1. Clear all storage for the site (Application → Storage → Clear site data).
  2. Reload the page with the analytics script active.
  3. Check each storage type for new entries.
Storage typeWhat to look forePrivacy trigger?
CookiesAny key set by the analytics domainYes
Local StoragelocalStorage keys from the trackerYes
Session StoragesessionStorage keysYes (tab lifetime)
IndexedDBDatabases created by the trackerYes

If the Application tab is clean, the tool may still be writing storage via server-side response headers. That is Check 3.

A few notes on what you might find with common tools:

  • GA4 client-side writes _ga (2-year JS cookie) and _ga_XXXXX (session cookie). Both are visible here.
  • Plausible, Fathom write nothing. The Application tab stays clean.
  • Matomo writes a first-party cookie by default; you can configure it to run without one.
  • sGTM with GA4 client may write nothing in the Application tab while setting FPID via a Set-Cookie header — which the Application tab does show in Cookies if it’s for the current domain.

Check 2: Which device APIs does the script read?

The Network tab shows HTTP requests; it cannot show JavaScript property reads. To catch fingerprinting — reads of navigator.hardwareConcurrency, canvas.toDataURL, AudioContext and similar — you need to intercept them in the Console before the page loads.

Open DevTools, go to Sources → Snippets (or use the Console tab), and paste this before any page load, or use Local Overrides to inject it on every reload:

;(function () {
	const log = []
	function trap(obj, key, label) {
		const d = Object.getOwnPropertyDescriptor(obj, key)
		if (!d) return
		Object.defineProperty(obj, key, {
			get() {
				log.push(label + '.' + key)
				return d.get ? d.get.call(this) : d.value
			},
			set: d.set,
			configurable: true
		})
	}
	;[
		[
			Navigator.prototype,
			['hardwareConcurrency', 'userAgent', 'language', 'languages', 'platform', 'deviceMemory'],
			'navigator'
		],
		[Screen.prototype, ['width', 'height', 'colorDepth', 'pixelDepth'], 'screen'],
		[HTMLCanvasElement.prototype, ['toDataURL', 'toBlob'], 'canvas']
	].forEach(([obj, keys, label]) => keys.forEach((k) => trap(obj, k, label)))
	const _AC = window.AudioContext || window.webkitAudioContext
	if (_AC) {
		window.AudioContext = window.webkitAudioContext = function (...args) {
			log.push('new AudioContext')
			return new _AC(...args)
		}
	}
	console.log('Watching navigator, screen, canvas, AudioContext — results in 5 s…')
	setTimeout(() => {
		console.log(
			log.length
				? 'APIs read:\n' + [...new Set(log)].join('\n')
				: 'No fingerprinting-API reads detected.'
		)
	}, 5000)
})()

Run this, then reload the page. After 5 seconds the console prints each API that any script on the page read. A cookieless tool that is not fingerprinting reads none of these. A fingerprinting library reads several, often including canvas.toDataURL (for font rendering) and navigator.hardwareConcurrency.

Reading navigator.userAgent and screen.width alone does not imply fingerprinting — these come through in standard HTTP headers too. The combination of canvas.toDataURL, AudioContext, and hardware properties is the fingerprinting signal.

Check 3: What does the server set via response headers?

A server can set a cookie without any JavaScript by including a Set-Cookie header in the HTTP response. This bypasses the Application tab’s Cookie section for pages you loaded with a clean cache, because the cookie arrives with the response. Reload the page with the Network tab open, filter by the analytics domain (for sGTM, your own domain), and look at response headers.

In the Network tab:

  1. Filter requests by the analytics endpoint domain using the filter bar.
  2. Click the tracking request (usually the first or second hit to the analytics domain).
  3. Open Headers → Response Headers and look for Set-Cookie.

For Server-Side GTM with the GA4 client template, the server writes two first-party cookies:

CookieLifetimePurpose
FPID2 yearsLong-term visitor ID across sessions
FPLC20 hoursShort-term cross-domain linker

FPID is set as HttpOnly; Secure; SameSite=Lax. Because it is server-set rather than JS-set, Safari ITP does not apply the 7-day deletion or the 24-hour link-decoration cap to it. It behaves like a normal first-party httpOnly cookie. That is the main reason sGTM improves data quality on Safari — not that it is “cookieless”, but that its cookies are treated better by the browser.

FPID stores a long-term visitor identifier on the device. It still falls under Art. 5(3) of the ePrivacy Directive and needs consent where that rule applies.

How long does a session ID survive?

Vendor documentation often quotes nominal lifetimes that browsers truncate in practice. The gaps matter most for Safari users.

Storage typeChromeSafari (ITP 2.3+)Firefox (ETP Strict)
JS-set cookie, no link decorationAs configured7 days without interactionStandard (first-party)
JS-set cookie, URL-decorated landing (gclid, fbclid, UTM)As configuredCapped at 24 hoursStandard
httpOnly cookie via Set-CookieAs configuredStandard first-party treatmentStandard (first-party)
localStorageUntil cleared7 days without interactionPartitioned for third-party; standard for first-party
sessionStorageTab lifetimeTab lifetimeTab lifetime

“7 days without interaction” means 7 days since the user last directly navigated to the site (typing the URL, clicking a bookmark, or following a non-decorated link). A returning organic search resets the clock. An email link with UTM parameters does not.

The practical impact on GA4 client-side is significant for sites with low return visit frequency. A visitor who comes back after 8 days on Safari registers as a new user, inflating new-user counts and breaking cohort analysis. sGTM’s FPID avoids this because it is httpOnly and server-set.

For Plausible and similar tools that build a server-side hash from IP + User-Agent + daily salt, the 7-day ITP window is irrelevant — there is no browser storage to delete. The session horizon is the salt rotation: 24 hours by definition.

Putting the two checks together

A vendor that clears Check 1 (no Application tab storage) and Check 2 (no fingerprinting APIs) and sets no Set-Cookie in Check 3 is genuinely stateless from the browser’s point of view. The session hash is built server-side from HTTP headers that the browser sends as part of normal routing.

That does not mean it is consent-exempt. The EU and UK consent rules post covers which countries have partial analytics exemptions, what conditions they require, and what changed in the UK in 2026.

Checking what your analytics stack actually collects — and wiring it up to fire correctly under consent — is part of my analytics work.

Frequently asked questions

Does Safari ITP break cookieless analytics?

Partially. Script-writable storage — localStorage, sessionStorage and JS-set cookies — is deleted after 7 days without user interaction on Safari. JS-set cookies on pages with URL decoration (gclid, fbclid, UTM parameters) are also capped at 24 hours. Server-set httpOnly cookies (like sGTM's FPID) are not subject to the same caps.

What is sGTM FPID and does it need a consent banner?

FPID (First Party ID) is a long-lived cookie set by the GA4 client in a Server-Side GTM container via a Set-Cookie response header. It identifies returning visitors across sessions. Because it stores data on the device it still falls under ePrivacy Art. 5(3) and needs the same consent treatment as a JS-set cookie.

Can the Network tab show which APIs a script reads?

No. The Network tab shows HTTP requests, not JavaScript API calls. To see which fingerprinting APIs a script reads — navigator.hardwareConcurrency, canvas.toDataURL, AudioContext — run a property-trapping snippet in the Console tab before the page loads.

Is localStorage truly permanent until cleared?

No, not on Safari. Safari ITP deletes script-writable storage including localStorage after 7 days of no user interaction with the site. Firefox partitions third-party storage. On Chrome, localStorage survives until the user clears it or the browser profile is deleted, but Chrome does not block third-party cookies by default for most users as of mid-2026.

Sources

  1. Webkit, Intelligent Tracking Prevention 2.3
  2. Webkit, Full Third-Party Cookie Blocking and More
  3. Plausible Analytics data policy
  4. Server-Side Tagging: First Party Cookies (Google Tag Platform)
  5. EDPB Guidelines 2/2023 on the technical scope of Art. 5(3) ePrivacy Directive, version 2.0
  6. URL Fragment Text Directives (WICG spec)