Introduction
The term “cookieless” has become the hottest buzzword in digital marketing and ad tech. You can’t attend a conference, read a vendor pitch, or scroll through LinkedIn without seeing someone touting their “cookieless solution.”
Google keeps postponing the deprecation of third-party cookies in Chrome. Safari and Firefox have already blocked them. The GDPR and ePrivacy regulations are tightening consent requirements. Everyone’s scrambling for alternatives.
But here’s the uncomfortable question nobody wants to ask: when a solution claims to be “cookieless,” what does that actually mean?
Spoiler alert: in most cases, it means “we’re still tracking you, we’ve just changed the method.”
In this deep dive, we’re not just going to talk theory. We’re going to open the browser DevTools, inspect actual cookies, examine local storage, write JavaScript to detect respawning, and show you exactly how these “cookieless” systems work under the hood.
By the end of this article, you’ll be able to audit any website and see for yourself what tracking techniques they’re really using.
Let’s dive deep.
1. Context: Why is everyone talking about “cookieless”?
To understand the cookieless movement, we need to understand what’s driving it.
Third-party cookie deprecation
Third-party cookies—those set by domains other than the one you’re visiting—have been the backbone of cross-site tracking for decades. But browsers are killing them:
- Safari (via Intelligent Tracking Prevention – ITP): Blocking third-party cookies since 2017, now limiting even first-party cookies to 7 days (or 24 hours for certain click-throughs)
- Firefox: Blocking third-party cookies by default since 2019
- Chrome: Originally planned for 2022, then 2023, then 2024… now postponed indefinitely but still coming
Regulatory pressure
The GDPR and ePrivacy Directive require explicit consent for non-essential cookies. The CNIL (French data protection authority) has issued guidelines that make persistent tracking harder to justify legally.
The industry’s response
Marketing tech vendors saw this coming and pivoted hard. The term “cookieless” became a competitive differentiator. “We’re future-proof!” “We don’t rely on cookies!” “We’re privacy-first!”
But as we’ll see, the reality is far more nuanced—and in many cases, misleading.
2. Master ID: The art of making identifiers persist
One of the most sophisticated “cookieless” techniques is the Master ID approach. Instead of relying on a single cookie, create a network of identifiers that back each other up.
2.1 Commanders Act: CAID + Phoenix
Commanders Act offers a perfect case study with their CAID (Commanders Act ID) system.
What is CAID?
CAID is their Master ID—a first-party cookie that serves as the primary user identifier. It’s a 24-character string combining the year and random numbers, set to expire after 12 months.
The cookies to look for
Open Chrome DevTools (F12) → Application → Cookies → select your domain.
Look for these Commanders Act cookies:
| Cookie Name | Purpose | Typical Duration |
|---|---|---|
CAID | Master ID (Commanders Act ID) | 12 months |
TCID or tc_id | Tag Commander ID (legacy) | 13 months |
TC_PRIVACY | Privacy consent settings | 13 months |
tC_CJ_* | Customer journey deduplication | Variable |
TC_PRIVACY_CENTER | Privacy center display preferences | 13 months |
How to inspect the CAID
javascript
// In browser console
document.cookie.split('; ').find(row => row.startsWith('CAID='))
// Example output:
// "CAID=2025123456789012345678"
// Year (2025) + 20 random digits
The “Phoenix” feature
Commanders Act doesn’t just set one cookie. They create what they call an “identifier graph”—multiple first-party cookies with different expiration dates that all point to the same Master ID.
From their documentation, Phoenix works by:
- Creating multiple cookies with staggered expiration dates
- Monitoring which cookies are present on each page load
- If any cookie from the graph is missing, regenerating it from the others
- Maintaining the Master ID as long as at least one cookie survives
Testing Phoenix regeneration
Try this experiment:
javascript
// Step 1: Note your current CAID
const originalCAID = document.cookie.split('; ')
.find(row => row.startsWith('CAID='))
?.split('=')[1];
console.log('Original CAID:', originalCAID);
// Step 2: Delete the CAID cookie
document.cookie = 'CAID=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
// Step 3: Verify it's gone
console.log('CAID after deletion:',
document.cookie.split('; ').find(row => row.startsWith('CAID=')));
// Should return undefined
// Step 4: Reload the page and check again
// (You'll need to reload manually)
// Step 5: After reload, check if CAID was regenerated
const newCAID = document.cookie.split('; ')
.find(row => row.startsWith('CAID='))
?.split('=')[1];
console.log('New CAID:', newCAID);
console.log('Match?', originalCAID === newCAID);
If Phoenix is active and other cookies in the graph are still present, the CAID will be regenerated with the same value.
Local Storage integration
Commanders Act also uses local storage as backup:
javascript
// Check local storage for Commanders Act data
Object.keys(localStorage).filter(key =>
key.includes('TC') || key.includes('commander')
).forEach(key => {
console.log(key, ':', localStorage.getItem(key));
});
// Common keys to look for:
// - tC.visitor_id
// - TC_PRIVACY
// - tC.id_sync_data
Complete audit script for Commanders Act
javascript
// Comprehensive Commanders Act tracking audit
function auditCommandersAct() {
console.log('=== COMMANDERS ACT AUDIT ===\n');
// 1. Check cookies
console.log('COOKIES:');
const cookies = document.cookie.split('; ');
const tcCookies = cookies.filter(c =>
c.startsWith('CAID') ||
c.startsWith('TC') ||
c.startsWith('tc_')
);
tcCookies.forEach(c => console.log(' ', c));
// 2. Check local storage
console.log('\nLOCAL STORAGE:');
Object.keys(localStorage).filter(key =>
key.toLowerCase().includes('tc') ||
key.toLowerCase().includes('commander')
).forEach(key => {
console.log(' ', key, ':', localStorage.getItem(key));
});
// 3. Check session storage
console.log('\nSESSION STORAGE:');
Object.keys(sessionStorage).filter(key =>
key.toLowerCase().includes('tc') ||
key.toLowerCase().includes('commander')
).forEach(key => {
console.log(' ', key, ':', sessionStorage.getItem(key));
});
// 4. Extract CAID if present
const caidCookie = cookies.find(c => c.startsWith('CAID='));
if (caidCookie) {
const caid = caidCookie.split('=')[1];
console.log('\n=== MASTER ID (CAID) ===');
console.log('Value:', caid);
console.log('Year:', caid.substring(0, 4));
console.log('Random ID:', caid.substring(4));
}
console.log('\n=== END AUDIT ===');
}
// Run the audit
auditCommandersAct();
2.2 Adobe: ECID ecosystem
Adobe takes a slightly different but equally sophisticated approach with their Experience Cloud ID (ECID).
The cookies to look for
Open DevTools → Application → Cookies:
| Cookie Name | Purpose | Typical Duration | Domain Type |
|---|---|---|---|
AMCV_{ORG_ID} | Experience Cloud Visitor ID (contains ECID) | 2 years (truncated to 13 months) | First-party |
s_ecid | ECID reference cookie | 2 years | First-party |
s_vi | Legacy Analytics visitor ID | 2 years (or 7 days on Safari) | First or third-party |
s_fid | Fallback ID when s_vi can’t be set | 2 years | First-party |
demdex | Cross-domain sync ID (UUID) | 180 days | Third-party (demdex.net) |
Finding your Adobe Organization ID
The {ORG_ID} in the AMCV cookie name is your Adobe Experience Cloud Organization ID.
javascript
// Find Adobe cookies
const adobeCookies = document.cookie.split('; ')
.filter(c => c.startsWith('AMCV_') || c.startsWith('s_'));
console.log('Adobe cookies found:', adobeCookies);
// Extract Organization ID from AMCV cookie
const amcvCookie = adobeCookies.find(c => c.startsWith('AMCV_'));
if (amcvCookie) {
const orgId = amcvCookie.split('=')[0].replace('AMCV_', '').split('%')[0];
console.log('Adobe Org ID:', orgId);
}
Decoding the AMCV cookie
The AMCV cookie contains multiple pieces of information encoded as pipe-separated key-value pairs:
javascript
// Extract and decode AMCV cookie
function decodeAMCV() {
const amcvCookie = document.cookie.split('; ')
.find(c => c.startsWith('AMCV_'));
if (!amcvCookie) {
console.log('No AMCV cookie found');
return;
}
const cookieValue = decodeURIComponent(amcvCookie.split('=')[1]);
const parts = cookieValue.split('|');
console.log('=== AMCV COOKIE STRUCTURE ===');
for (let i = 0; i < parts.length; i += 2) {
const key = parts[i];
const value = parts[i + 1];
console.log(`${key}: ${value}`);
// Highlight the ECID
if (key === 'MCMID' || key === 'MID') {
console.log(' ^^^ THIS IS YOUR ECID (Experience Cloud ID)');
}
}
}
decodeAMCV();
// Example output:
// MCMID: 12345678901234567890123456789012345678
// ^^^ THIS IS YOUR ECID (Experience Cloud ID)
// MCAAMLH: 9
// MCAAMB: base64encodedvalue...
// MCOPTOUT: ...
Extracting the ECID programmatically
If Adobe’s Visitor API is loaded, you can access the ECID directly:
javascript
// Method 1: Using Adobe Visitor API (if available)
if (typeof Visitor !== 'undefined' && window.s && window.s.visitor) {
const ecid = window.s.visitor.getMarketingCloudVisitorID();
console.log('ECID from Visitor API:', ecid);
}
// Method 2: Parse from AMCV cookie manually
function getECIDFromCookie() {
const amcvCookie = document.cookie.split('; ')
.find(c => c.startsWith('AMCV_'));
if (!amcvCookie) return null;
const cookieValue = decodeURIComponent(amcvCookie.split('=')[1]);
const parts = cookieValue.split('|');
// Find MCMID or MID (both refer to ECID)
for (let i = 0; i < parts.length; i += 2) {
if (parts[i] === 'MCMID' || parts[i] === 'MID') {
return parts[i + 1];
}
}
return null;
}
console.log('ECID from cookie:', getECIDFromCookie());
Testing FPID implementation
First-Party Device ID (FPID) is set server-side by the customer and used to seed the ECID:
javascript
// Check if FPID is being used
function checkFPID() {
// FPID cookie is typically named something like "FPID" or custom name
const fpidCookie = document.cookie.split('; ')
.find(c => c.startsWith('FPID='));
if (fpidCookie) {
console.log('FPID detected:', fpidCookie.split('=')[1]);
console.log('FPID is being used to seed ECID');
} else {
console.log('No FPID cookie found - using standard ECID generation');
}
// Check if Web SDK is loaded (required for FPID)
if (typeof alloy !== 'undefined') {
console.log('Adobe Web SDK (alloy) detected - FPID supported');
} else if (typeof s !== 'undefined' && s.version) {
console.log('Legacy AppMeasurement detected - FPID NOT supported');
console.log('AppMeasurement version:', s.version);
}
}
checkFPID();
The ITP problem visualized
javascript
// Check cookie expiration and ITP impact
function checkAdobeCookieExpiration() {
console.log('=== ADOBE COOKIE EXPIRATION CHECK ===\n');
// Get all cookies (Note: JavaScript can't read actual expiration dates)
// But we can check if cookies exist and when they were last set
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
console.log('Browser:', isSafari ? 'Safari (ITP active)' : 'Other');
if (isSafari) {
console.log('⚠️ WARNING: Safari ITP limits first-party cookies to 7 days');
console.log('⚠️ Ad click-through cookies limited to 24 hours');
console.log('💡 Adobe FPID recommended for this browser');
}
// Check which cookies are present
const hasAMCV = document.cookie.includes('AMCV_');
const hasSVI = document.cookie.includes('s_vi');
const hasSFID = document.cookie.includes('s_fid');
const hasFPID = document.cookie.includes('FPID');
console.log('\nCookie presence:');
console.log(' AMCV (ECID):', hasAMCV ? '✓' : '✗');
console.log(' s_vi (legacy):', hasSVI ? '✓' : '✗');
console.log(' s_fid (fallback):', hasSFID ? '✓' : '✗');
console.log(' FPID (server-side seed):', hasFPID ? '✓' : '✗');
}
checkAdobeCookieExpiration();
Complete Adobe audit script
javascript
// Comprehensive Adobe tracking audit
function auditAdobe() {
console.log('=== ADOBE EXPERIENCE CLOUD AUDIT ===\n');
// 1. Identify all Adobe cookies
console.log('COOKIES:');
const cookies = document.cookie.split('; ');
const adobeCookies = cookies.filter(c =>
c.startsWith('AMCV_') ||
c.startsWith('s_') ||
c.includes('demdex') ||
c.startsWith('FPID')
);
adobeCookies.forEach(c => {
const [name, value] = c.split('=');
console.log(` ${name}:`, value.substring(0, 50) + (value.length > 50 ? '...' : ''));
});
// 2. Extract ECID
console.log('\n=== ECID EXTRACTION ===');
const ecid = getECIDFromCookie();
if (ecid) {
console.log('ECID:', ecid);
console.log('Length:', ecid.length, 'characters');
} else {
console.log('No ECID found');
}
// 3. Check Adobe libraries
console.log('\n=== ADOBE LIBRARIES ===');
console.log('Visitor API:', typeof Visitor !== 'undefined' ? '✓ Loaded' : '✗ Not loaded');
console.log('AppMeasurement:', typeof s !== 'undefined' ? `✓ Loaded (v${s.version})` : '✗ Not loaded');
console.log('Web SDK (alloy):', typeof alloy !== 'undefined' ? '✓ Loaded' : '✗ Not loaded');
// 4. Check for FPID
console.log('\n=== FPID CHECK ===');
checkFPID();
// 5. Browser compatibility
console.log('\n=== BROWSER COMPATIBILITY ===');
checkAdobeCookieExpiration();
console.log('\n=== END AUDIT ===');
}
// Run the audit
auditAdobe();
3. Local Storage: The cookie that doesn’t call itself one
Here’s a favorite trick: stop using cookies and start using local storage. Problem solved, right?
Not quite.
What is local storage?
Local storage is an HTML5 web storage mechanism that allows websites to store data in the browser. Unlike cookies:
- No expiration date: Data persists indefinitely until manually deleted
- Larger capacity: 5-10MB vs 4KB for cookies
- Not sent with HTTP requests: Data stays client-side unless explicitly transmitted
Inspecting local storage
Open DevTools → Application → Local Storage → select your domain.
You’ll see key-value pairs. Look for tracking-related keys.
Common patterns to spot
javascript
// List all local storage keys
console.log('=== LOCAL STORAGE KEYS ===');
Object.keys(localStorage).forEach(key => {
console.log(key, ':', localStorage.getItem(key));
});
// Common tracking-related key patterns:
// - user_id, userId, visitor_id, visitorId
// - session_id, sessionId
// - _ga, _gid (Google Analytics)
// - amplitude_*, mixpanel_*
// - Any UUID-looking values
Example: Google Analytics in local storage
Google Analytics 4 uses local storage extensively:
javascript
// Check for GA4 local storage
Object.keys(localStorage).filter(key => key.startsWith('_ga')).forEach(key => {
console.log(key, ':', localStorage.getItem(key));
});
// Common GA4 local storage keys:
// _ga_XXXXXXXXXX : Stores client ID and session info
// _ga : Cross-property client ID
The mutual backup strategy
Many tracking solutions store the same identifier in both a cookie and local storage:
javascript
// Example tracking implementation with dual storage
function setTrackingID(id) {
// Store in cookie
document.cookie = `user_id=${id}; max-age=31536000; path=/`;
// Also store in local storage
localStorage.setItem('user_id', id);
console.log('Tracking ID stored in both cookie and local storage');
}
// Retrieve with fallback
function getTrackingID() {
// Try cookie first
const cookieMatch = document.cookie.match(/user_id=([^;]+)/);
if (cookieMatch) return cookieMatch[1];
// Fall back to local storage
return localStorage.getItem('user_id');
}
// Sync function (runs on page load)
function syncTrackingID() {
const cookieID = document.cookie.match(/user_id=([^;]+)/)?.[1];
const storageID = localStorage.getItem('user_id');
if (cookieID && !storageID) {
// Cookie exists but local storage doesn't - copy to storage
localStorage.setItem('user_id', cookieID);
console.log('Restored local storage from cookie');
} else if (storageID && !cookieID) {
// Local storage exists but cookie doesn't - recreate cookie
document.cookie = `user_id=${storageID}; max-age=31536000; path=/`;
console.log('🔄 COOKIE RESPAWNED from local storage!');
} else if (cookieID !== storageID) {
// Conflict - use most recent (in this case, cookie wins)
localStorage.setItem('user_id', cookieID);
}
}
// This sync function runs on every page load
syncTrackingID();
Testing for cookie-local storage backup
javascript
// Test if a site uses local storage backup for cookies
function testLocalStorageBackup() {
console.log('=== TESTING LOCAL STORAGE BACKUP ===\n');
// Step 1: Identify a tracking cookie
const trackingCookie = document.cookie.split('; ')
.find(c => c.includes('id') || c.includes('ID'));
if (!trackingCookie) {
console.log('No obvious tracking cookie found');
return;
}
const [cookieName, cookieValue] = trackingCookie.split('=');
console.log('Found tracking cookie:', cookieName, '=', cookieValue);
// Step 2: Check if same value exists in local storage
let foundInStorage = false;
Object.keys(localStorage).forEach(key => {
const value = localStorage.getItem(key);
if (value === cookieValue || value.includes(cookieValue)) {
console.log('✓ FOUND IN LOCAL STORAGE:', key, '=', value);
foundInStorage = true;
}
});
if (!foundInStorage) {
console.log('Value not found in local storage (yet)');
}
// Step 3: Delete the cookie and reload to see if it respawns
console.log('\n📝 EXPERIMENT:');
console.log('1. Delete this cookie:', cookieName);
console.log('2. Reload the page');
console.log('3. Check if cookie reappears with same value');
console.log('\nTo delete cookie, run:');
console.log(`document.cookie = '${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'`);
}
testLocalStorageBackup();
Detecting IndexedDB usage
Some sites use IndexedDB for even more persistent storage:
javascript
// Check for IndexedDB usage
async function checkIndexedDB() {
console.log('=== INDEXEDDB CHECK ===\n');
const databases = await indexedDB.databases();
if (databases.length === 0) {
console.log('No IndexedDB databases found');
return;
}
console.log(`Found ${databases.length} IndexedDB database(s):`);
databases.forEach(db => {
console.log(` - ${db.name} (v${db.version})`);
});
console.log('\n💡 IndexedDB can store tracking data invisible to cookie inspection');
}
checkIndexedDB();
Complete storage audit script
javascript
// Comprehensive storage audit (cookies + local + session + IndexedDB)
async function auditAllStorage() {
console.log('=== COMPLETE STORAGE AUDIT ===\n');
// 1. Cookies
console.log('COOKIES (' + document.cookie.split('; ').length + '):');
document.cookie.split('; ').forEach(c => {
const [name, value] = c.split('=');
console.log(` ${name}: ${value.substring(0, 40)}${value.length > 40 ? '...' : ''}`);
});
// 2. Local Storage
console.log('\nLOCAL STORAGE (' + Object.keys(localStorage).length + ' keys):');
Object.keys(localStorage).forEach(key => {
const value = localStorage.getItem(key);
console.log(` ${key}: ${value.substring(0, 40)}${value.length > 40 ? '...' : ''}`);
});
// 3. Session Storage
console.log('\nSESSION STORAGE (' + Object.keys(sessionStorage).length + ' keys):');
Object.keys(sessionStorage).forEach(key => {
const value = sessionStorage.getItem(key);
console.log(` ${key}: ${value.substring(0, 40)}${value.length > 40 ? '...' : ''}`);
});
// 4. IndexedDB
console.log('\nINDEXEDDB:');
const databases = await indexedDB.databases();
if (databases.length > 0) {
databases.forEach(db => console.log(` ${db.name} (v${db.version})`));
} else {
console.log(' None');
}
// 5. Calculate total storage
let totalSize = 0;
totalSize += new Blob([document.cookie]).size;
totalSize += new Blob(Object.values(localStorage)).size;
totalSize += new Blob(Object.values(sessionStorage)).size;
console.log('\nAPPROXIMATE STORAGE SIZE:', (totalSize / 1024).toFixed(2), 'KB');
console.log('\n=== END AUDIT ===');
}
auditAllStorage();
4. Cookie Respawning: Resurrection techniques
Now we get into the really sophisticated stuff: techniques that actively resurrect deleted cookies.
4.1 The principle
Cookie respawning (also called “evercookies” or “zombie cookies”) is the practice of recreating a deleted cookie using backup data stored elsewhere.
The user thinks they’ve deleted their tracking cookie. But the website has secretly stored the same identifier in multiple places. When the site detects the cookie is gone, it automatically recreates it from the backup.
4.2 Detecting cookie respawning in real-time
Let’s build a detector that monitors for cookie respawning:
javascript
// Real-time cookie respawning detector
class CookieRespawnDetector {
constructor() {
this.cookieSnapshot = {};
this.respawnEvents = [];
}
// Take initial snapshot of all cookies
takeSnapshot() {
this.cookieSnapshot = {};
document.cookie.split('; ').forEach(cookie => {
const [name, value] = cookie.split('=');
this.cookieSnapshot[name] = value;
});
console.log('📸 Snapshot taken:', Object.keys(this.cookieSnapshot).length, 'cookies');
}
// Delete all cookies
deleteAllCookies() {
console.log('🗑️ Deleting all cookies...');
document.cookie.split('; ').forEach(cookie => {
const name = cookie.split('=')[0];
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
// Also try with domain variants
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${location.hostname}`;
});
console.log('✓ Cookies deleted');
}
// Check for respawned cookies
checkForRespawn() {
const currentCookies = {};
document.cookie.split('; ').forEach(cookie => {
if (cookie.trim()) {
const [name, value] = cookie.split('=');
currentCookies[name] = value;
}
});
// Compare with snapshot
Object.keys(this.cookieSnapshot).forEach(name => {
if (currentCookies[name] === this.cookieSnapshot[name]) {
const event = {
name: name,
value: currentCookies[name],
timestamp: new Date(),
type: 'respawned'
};
this.respawnEvents.push(event);
console.log('🧟 RESPAWNED COOKIE DETECTED!', name, '=', currentCookies[name]);
}
});
return this.respawnEvents;
}
// Start monitoring
startMonitoring(intervalMs = 1000) {
console.log('👀 Starting respawn monitoring...');
this.takeSnapshot();
this.deleteAllCookies();
let checks = 0;
const maxChecks = 30; // Monitor for 30 seconds
const interval = setInterval(() => {
checks++;
console.log(`Check ${checks}/${maxChecks}...`);
this.checkForRespawn();
if (checks >= maxChecks) {
clearInterval(interval);
this.reportResults();
}
}, intervalMs);
}
// Report results
reportResults() {
console.log('\n=== RESPAWN DETECTION RESULTS ===');
if (this.respawnEvents.length === 0) {
console.log('✓ No cookie respawning detected');
} else {
console.log(`⚠️ ${this.respawnEvents.length} cookie(s) respawned:`);
this.respawnEvents.forEach(event => {
console.log(` - ${event.name} (at ${event.timestamp.toLocaleTimeString()})`);
});
console.log('\n🚨 This site is using cookie respawning techniques!');
}
}
}
// Usage:
const detector = new CookieRespawnDetector();
detector.startMonitoring();
4.3 Browser fingerprinting for respawning
This is the most sophisticated technique, and it’s surprisingly common.
How it works
Instead of storing the identifier client-side, the server creates a “fingerprint” of your browser and associates it with your tracking ID.
Collecting browser fingerprint data
Here’s what a fingerprinting script might collect:
javascript
// Browser fingerprinting data collection (example)
function collectFingerprint() {
const fingerprint = {
// Basic browser info
userAgent: navigator.userAgent,
language: navigator.language,
languages: navigator.languages,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemory: navigator.deviceMemory,
// Screen info
screenResolution: `${screen.width}x${screen.height}`,
screenDepth: screen.colorDepth,
availableScreenSize: `${screen.availWidth}x${screen.availHeight}`,
// Timezone
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezoneOffset: new Date().getTimezoneOffset(),
// Canvas fingerprint (simplified)
canvas: getCanvasFingerprint(),
// WebGL fingerprint (simplified)
webgl: getWebGLFingerprint(),
// Installed plugins (deprecated in modern browsers)
plugins: Array.from(navigator.plugins).map(p => p.name),
// Do Not Track
doNotTrack: navigator.doNotTrack,
// Fonts (requires additional library)
// fonts: detectFonts(),
// Touch support
touchSupport: 'ontouchstart' in window,
maxTouchPoints: navigator.maxTouchPoints,
};
return fingerprint;
}
// Canvas fingerprinting
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('Browser fingerprint', 2, 2);
return canvas.toDataURL();
}
// WebGL fingerprinting
function getWebGLFingerprint() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
if (!gl) return null;
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
return {
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
};
}
// Generate a hash from fingerprint (simple example)
function hashFingerprint(fingerprint) {
const str = JSON.stringify(fingerprint);
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString(36);
}
// Usage
const fingerprint = collectFingerprint();
const fingerprintHash = hashFingerprint(fingerprint);
console.log('Fingerprint hash:', fingerprintHash);
console.log('Full fingerprint:', fingerprint);
Detecting fingerprinting activity
javascript
// Detect if a site is fingerprinting you
function detectFingerprinting() {
console.log('=== FINGERPRINTING DETECTION ===\n');
let fingerprintingSignals = [];
// Check for canvas fingerprinting
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function() {
fingerprintingSignals.push({
type: 'canvas',
method: 'toDataURL',
stack: new Error().stack
});
console.log('🎨 Canvas fingerprinting detected!');
return originalToDataURL.apply(this, arguments);
};
// Check for WebGL fingerprinting
const originalGetParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {
const debugInfo = {
37445: 'UNMASKED_VENDOR_WEBGL',
37446: 'UNMASKED_RENDERER_WEBGL'
};
if (debugInfo[param]) {
fingerprintingSignals.push({
type: 'webgl',
parameter: debugInfo[param]
});
console.log('🖼️ WebGL fingerprinting detected! Parameter:', debugInfo[param]);
}
return originalGetParameter.apply(this, arguments);
};
// Monitor for 10 seconds then report
setTimeout(() => {
console.log('\n=== FINGERPRINTING REPORT ===');
if (fingerprintingSignals.length === 0) {
console.log('✓ No fingerprinting detected');
} else {
console.log(`⚠️ ${fingerprintingSignals.length} fingerprinting signals detected:`);
fingerprintingSignals.forEach((signal, i) => {
console.log(`${i+1}. ${signal.type.toUpperCase()}:`, signal);
});
}
}, 10000);
console.log('Monitoring for fingerprinting... (10 seconds)');
}
// Run detection
detectFingerprinting();
Network requests indicating fingerprinting
Look for these patterns in DevTools → Network:
javascript
// Monitor network requests for fingerprinting endpoints
function monitorFingerprintingRequests() {
// Common fingerprinting endpoint patterns
const patterns = [
/fingerprint/i,
/canvas/i,
/webgl/i,
/font.*detect/i,
/device.*info/i,
/browser.*info/i
];
// Override fetch
const originalFetch = window.fetch;
window.fetch = function() {
const url = arguments[0];
const urlString = typeof url === 'string' ? url : url.url;
patterns.forEach(pattern => {
if (pattern.test(urlString)) {
console.log('🔍 Potential fingerprinting request:', urlString);
}
});
return originalFetch.apply(this, arguments);
};
// Override XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
patterns.forEach(pattern => {
if (pattern.test(url)) {
console.log('🔍 Potential fingerprinting XHR:', url);
}
});
return originalOpen.apply(this, arguments);
};
console.log('👀 Monitoring network requests for fingerprinting...');
}
monitorFingerprintingRequests();
4.4 Complete respawning detection suite
javascript
// Ultimate cookie respawning and fingerprinting detection suite
class TrackingDetector {
constructor() {
this.results = {
cookieRespawning: [],
fingerprinting: [],
localStorage: [],
networkRequests: []
};
}
// Run all detection methods
async runFullScan() {
console.log('🔬 Starting comprehensive tracking detection...\n');
this.detectCookieRespawning();
this.detectFingerprinting();
this.detectLocalStorageBackup();
this.monitorNetworkRequests();
// Wait 30 seconds then report
setTimeout(() => {
this.generateReport();
}, 30000);
console.log('⏱️ Scan running for 30 seconds...\n');
}
detectCookieRespawning() {
const detector = new CookieRespawnDetector();
detector.startMonitoring(2000); // Check every 2 seconds
this.results.cookieRespawning = detector.respawnEvents;
}
detectFingerprinting() {
// Canvas detection
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function() {
this.results.fingerprinting.push({type: 'canvas', timestamp: new Date()});
return originalToDataURL.apply(this, arguments);
}.bind(this);
// WebGL detection
const originalGetParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {
if (param === 37445 || param === 37446) {
this.results.fingerprinting.push({type: 'webgl', timestamp: new Date()});
}
return originalGetParameter.apply(this, arguments);
}.bind(this);
}
detectLocalStorageBackup() {
// Check if cookie values exist in localStorage
const cookies = document.cookie.split('; ').reduce((acc, c) => {
const [name, value] = c.split('=');
acc[name] = value;
return acc;
}, {});
Object.keys(localStorage).forEach(key => {
const value = localStorage.getItem(key);
Object.entries(cookies).forEach(([cookieName, cookieValue]) => {
if (value.includes(cookieValue) || cookieValue.includes(value)) {
this.results.localStorage.push({
localStorageKey: key,
cookieName: cookieName,
matched: true
});
}
});
});
}
monitorNetworkRequests() {
const patterns = [/fingerprint/i, /canvas/i, /webgl/i, /track/i, /collect/i];
const originalFetch = window.fetch;
window.fetch = function() {
const url = arguments[0];
const urlString = typeof url === 'string' ? url : url.url;
patterns.forEach(pattern => {
if (pattern.test(urlString)) {
this.results.networkRequests.push({url: urlString, timestamp: new Date()});
}
});
return originalFetch.apply(this, arguments);
}.bind(this);
}
generateReport() {
console.log('\n═══════════════════════════════════════');
console.log(' TRACKING DETECTION REPORT');
console.log('═══════════════════════════════════════\n');
// Cookie respawning
console.log('🧟 COOKIE RESPAWNING:');
if (this.results.cookieRespawning.length === 0) {
console.log(' ✓ None detected');
} else {
console.log(` ⚠️ ${this.results.cookieRespawning.length} instances`);
this.results.cookieRespawning.forEach(e => console.log(` - ${e.name}`));
}
// Fingerprinting
console.log('\n🔍 FINGERPRINTING:');
if (this.results.fingerprinting.length === 0) {
console.log(' ✓ None detected');
} else {
console.log(` ⚠️ ${this.results.fingerprinting.length} instances`);
const byType = this.results.fingerprinting.reduce((acc, f) => {
acc[f.type] = (acc[f.type] || 0) + 1;
return acc;
}, {});
Object.entries(byType).forEach(([type, count]) => {
console.log(` - ${type}: ${count}x`);
});
}
// Local storage backup
console.log('\n💾 LOCAL STORAGE BACKUP:');
if (this.results.localStorage.length === 0) {
console.log(' ✓ No cookie-localStorage pairing detected');
} else {
console.log(` ⚠️ ${this.results.localStorage.length} pairings found`);
this.results.localStorage.forEach(p => {
console.log(` - ${p.localStorageKey} ↔ ${p.cookieName}`);
});
}
// Network requests
console.log('\n🌐 SUSPICIOUS NETWORK REQUESTS:');
if (this.results.networkRequests.length === 0) {
console.log(' ✓ None detected');
} else {
console.log(` ⚠️ ${this.results.networkRequests.length} requests`);
this.results.networkRequests.slice(0, 5).forEach(r => {
console.log(` - ${r.url.substring(0, 60)}...`);
});
}
// Overall verdict
console.log('\n═══════════════════════════════════════');
const totalIssues = this.results.cookieRespawning.length +
this.results.fingerprinting.length +
this.results.localStorage.length +
this.results.networkRequests.length;
if (totalIssues === 0) {
console.log('✅ VERDICT: No aggressive tracking detected');
} else if (totalIssues < 5) {
console.log('⚠️ VERDICT: Some tracking techniques detected');
} else {
console.log('🚨 VERDICT: Multiple aggressive tracking techniques in use!');
}
console.log('═══════════════════════════════════════\n');
}
}
// Run the full scan
const scanner = new TrackingDetector();
scanner.runFullScan();
5. The reality on the ground: What “cookieless” actually means
Let’s be clear about the spectrum of “cookieless” claims:
Level 1: “No third-party cookies”
“We only use first-party cookies!”
This is the most common claim. Technically accurate—they’re not using third-party cookies. But they’re absolutely still using cookies for persistent tracking, just on the first-party domain.
How to verify:
javascript
// Check if cookies are first-party or third-party
document.cookie.split('; ').forEach(cookie => {
const name = cookie.split('=')[0];
console.log(`${name}: first-party (by definition, since we can read it)`);
});
// Third-party cookies can't be accessed via JavaScript from the current page
// They're only visible in DevTools → Application → Cookies → other domains
Level 2: “No client-side cookies”
“We don’t set cookies with JavaScript!”
Instead, cookies are set server-side via HTTP Set-Cookie headers.
How to verify:
javascript
// Check Network tab for Set-Cookie headers
// DevTools → Network → select a request → Headers → Response Headers
// Look for "Set-Cookie"
Level 3: “No cookies, just local storage”
“We use local storage instead of cookies!”
As discussed earlier, this is privacy theater.
How to verify:
javascript
// Already covered in section 3
auditAllStorage();
Level 4: “Server-side tracking”
“All tracking happens server-side!”
The identifier might not be stored client-side at all. Instead, server-side session management uses IP + User Agent + other request headers.
How to verify: Look for tracking parameters in URLs:
javascript
// Check for tracking parameters in current URL
const url = new URL(window.location.href);
const trackingParams = ['utm_', 'fbclid', 'gclid', '_ga', 'mc_', 'msclkid'];
console.log('Tracking parameters in URL:');
trackingParams.forEach(param => {
url.searchParams.forEach((value, key) => {
if (key.startsWith(param)) {
console.log(` ${key}: ${value}`);
}
});
});
Level 5: “Pure fingerprinting”
“We don’t store anything client-side!”
Already covered in section 4.3.
6. The legal questions (I’m not a lawyer, but…)
The GDPR applies to tracking, not just cookies
The GDPR regulates the processing of personal data. An advertising ID, analytics ID, or any persistent identifier that can track you across sessions is personal data.
It doesn’t matter if it’s stored in:
- A cookie
- Local storage
- Server-side only
- Generated from browser fingerprinting
If it identifies you, it’s regulated.
What about local storage?
The ePrivacy Directive (the “Cookie Law”) specifically mentions “cookies and similar technologies.”
The CNIL’s position: local storage used for tracking or advertising purposes requires consent, just like cookies.
Is fingerprinting legal?
Browser fingerprinting processes device and browser characteristics to create a unique identifier. Under the GDPR, this is personal data processing.
The CNIL has stated that fingerprinting for tracking purposes requires consent.
Cookie respawning = consent bypass?
If a user deletes a tracking cookie, they’ve clearly indicated they don’t want to be tracked.
Automatically recreating that cookie from backup storage or fingerprinting data arguably:
- Violates GDPR Article 7(3) (withdrawal of consent)
- Violates ePrivacy Directive requirements
- Constitutes unlawful processing
Potential penalties: up to €20 million or 4% of global annual revenue.
Yet hundreds of major websites do this every day.
7. What really matters: Transparency
Here’s my take after 10 years in ad tech and analytics:
“Cookieless” has become a marketing wording
The term no longer means what it says. It’s a competitive differentiator that obscures more than it clarifies.
When a vendor says “cookieless,” you need to ask:
- What storage mechanisms are you using?
- Are you using server-side identifiers?
- Are you using fingerprinting?
- How long do identifiers persist?
- What happens when a user deletes their data?
- Can users actually opt out?
User identification isn’t inherently evil
There are legitimate reasons for persistent user identification:
- Session management
- Personalization
- Analytics
- Attribution
- Fraud prevention
But transparency and respect must come first
If you’re going to identify users:
- Tell them clearly what you’re doing
- Explain why you’re doing it
- Give them real control over their data
- Respect their choices when they opt out
Don’t claim to be “cookieless” while using six different persistence techniques.
Don’t regenerate identifiers after users delete them.
Don’t hide behind technical jargon.
Conclusion
“Cookieless” exists on a spectrum. It’s not binary.
The techniques we’ve covered—Master IDs, Phoenix resurrection, local storage backups, fingerprinting, cookie respawning—are all real. They’re used by major platforms serving billions of users.
Now you have the tools to audit any website yourself:
- Inspector scripts for Commanders Act and Adobe
- Storage audit tools
- Cookie respawning detectors
- Fingerprinting monitors
- Complete tracking detection suites