See what advertising/tracking scripts observe about you. Passive, local-only logging of fingerprinting attempts, third-party trackers, and cookie syncing — with a built-in report dashboard.
// ==UserScript==
// @name AdTech Mirror
// @namespace https://tampermonkey.net/
// @version 1.0
// @description See what advertising/tracking scripts observe about you. Passive, local-only logging of fingerprinting attempts, third-party trackers, and cookie syncing — with a built-in report dashboard.
// @author F1xL1T
// @license MIT
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const DB_NAME = 'AdTechMirrorDB';
const DB_VERSION = 1;
const STORE_EVENTS = 'events';
const RETENTION_DAYS = 90;
// -------------------------------------------------------------------
// Known tracker domains (category-tagged). Not exhaustive — covers
// the most common ad/analytics/fingerprinting vendors.
// -------------------------------------------------------------------
const TRACKER_DOMAINS = [
// Google / Alphabet
{ match: 'doubleclick.net', name: 'Google DoubleClick (DSP/Ad Exchange)', category: 'ad-exchange' },
{ match: 'googlesyndication.com', name: 'Google AdSense', category: 'ad-network' },
{ match: 'googletagmanager.com', name: 'Google Tag Manager', category: 'tag-manager' },
{ match: 'google-analytics.com', name: 'Google Analytics', category: 'analytics' },
{ match: 'googleadservices.com', name: 'Google Ads', category: 'ad-network' },
{ match: 'adservice.google', name: 'Google Ad Service', category: 'ad-network' },
// Meta
{ match: 'facebook.com/tr', name: 'Meta Pixel', category: 'pixel' },
{ match: 'connect.facebook.net', name: 'Meta Pixel SDK', category: 'pixel' },
// TikTok
{ match: 'analytics.tiktok.com', name: 'TikTok Pixel', category: 'pixel' },
{ match: 'business-api.tiktok.com', name: 'TikTok Business API', category: 'pixel' },
// Amazon
{ match: 'amazon-adsystem.com', name: 'Amazon Advertising (DSP)', category: 'ad-network' },
// Microsoft
{ match: 'bat.bing.com', name: 'Microsoft UET (Bing Ads)', category: 'pixel' },
{ match: 'clarity.ms', name: 'Microsoft Clarity', category: 'analytics' },
// Major SSPs / Exchanges / DMPs
{ match: 'criteo.com', name: 'Criteo', category: 'ad-exchange' },
{ match: 'taboola.com', name: 'Taboola', category: 'ad-network' },
{ match: 'outbrain.com', name: 'Outbrain', category: 'ad-network' },
{ match: 'pubmatic.com', name: 'PubMatic', category: 'ssp' },
{ match: 'rubiconproject.com', name: 'Magnite (Rubicon Project)', category: 'ssp' },
{ match: 'openx.net', name: 'OpenX', category: 'ssp' },
{ match: 'adnxs.com', name: 'AppNexus / Xandr', category: 'ad-exchange' },
{ match: 'casalemedia.com', name: 'Index Exchange', category: 'ssp' },
{ match: 'smartadserver.com', name: 'Smart AdServer (Equativ)', category: 'ad-exchange' },
{ match: 'sharethrough.com', name: 'Sharethrough', category: 'ssp' },
{ match: 'media.net', name: 'Media.net', category: 'ad-network' },
{ match: 'adform.net', name: 'Adform', category: 'dsp' },
{ match: 'theadex.com', name: 'Adex (Adition)', category: 'dmp' },
{ match: 'bidswitch.net', name: 'BidSwitch', category: 'ad-exchange' },
{ match: 'contextweb.com', name: 'PulsePoint', category: 'ssp' },
{ match: 'yieldmo.com', name: 'Yieldmo', category: 'ssp' },
{ match: 'triplelift.com', name: 'TripleLift', category: 'ssp' },
{ match: 'sovrn.com', name: 'Sovrn', category: 'ssp' },
{ match: 'gumgum.com', name: 'GumGum', category: 'ad-network' },
// Data brokers / identity / DMPs
{ match: 'liveramp.com', name: 'LiveRamp (Identity / Data Broker)', category: 'identity' },
{ match: 'id5-sync.com', name: 'ID5 (Identity Sync)', category: 'identity' },
{ match: 'adsrvr.org', name: 'The Trade Desk (DSP)', category: 'dsp' },
{ match: 'agkn.com', name: 'Neustar / TransUnion (Data Broker)', category: 'data-broker' },
{ match: 'demdex.net', name: 'Adobe Audience Manager (DMP)', category: 'dmp' },
{ match: 'crwdcntrl.net', name: 'LiveRamp / Lotame (DMP)', category: 'dmp' },
{ match: 'bluekai.com', name: 'Oracle BlueKai (DMP)', category: 'dmp' },
{ match: 'mathtag.com', name: 'MediaMath (DSP)', category: 'dsp' },
{ match: 'tapad.com', name: 'Tapad (Cross-device Identity)', category: 'identity' },
// Analytics / session recording / heatmaps
{ match: 'hotjar.com', name: 'Hotjar (Session Recording)', category: 'analytics' },
{ match: 'mouseflow.com', name: 'Mouseflow (Session Recording)', category: 'analytics' },
{ match: 'fullstory.com', name: 'FullStory (Session Recording)', category: 'analytics' },
{ match: 'segment.io', name: 'Segment (CDP)', category: 'cdp' },
{ match: 'segment.com', name: 'Segment (CDP)', category: 'cdp' },
{ match: 'mixpanel.com', name: 'Mixpanel', category: 'analytics' },
{ match: 'amplitude.com', name: 'Amplitude', category: 'analytics' },
// Fingerprinting-as-a-service
{ match: 'fingerprintjs.com', name: 'FingerprintJS', category: 'fingerprint-vendor' },
{ match: 'fpjs.io', name: 'FingerprintJS (CDN)', category: 'fingerprint-vendor' },
{ match: 'iovation.com', name: 'TransUnion iovation (Device ID)', category: 'fingerprint-vendor' },
{ match: 'maxmind.com', name: 'MaxMind (IP Intelligence)', category: 'data-broker' },
{ match: 'perimeterx.net', name: 'PerimeterX / HUMAN (Bot/Fingerprint)', category: 'fingerprint-vendor' },
{ match: 'forter.com', name: 'Forter (Fraud/Fingerprint)', category: 'fingerprint-vendor' },
// Other common pixels
{ match: 'snapchat.com/p', name: 'Snap Pixel', category: 'pixel' },
{ match: 'sc-static.net', name: 'Snap Pixel SDK', category: 'pixel' },
{ match: 'pinterest.com/v3', name: 'Pinterest Tag', category: 'pixel' },
{ match: 'ads-twitter.com', name: 'X (Twitter) Pixel', category: 'pixel' },
{ match: 'linkedin.com/px', name: 'LinkedIn Insight Tag', category: 'pixel' },
{ match: 'reddit.com/rp', name: 'Reddit Pixel', category: 'pixel' },
{ match: 'quantserve.com', name: 'Quantcast', category: 'analytics' },
{ match: 'scorecardresearch.com', name: 'Comscore', category: 'analytics' },
{ match: 'addthis.com', name: 'AddThis', category: 'analytics' },
{ match: 'newrelic.com', name: 'New Relic', category: 'analytics' },
];
function classifyDomain(hostname) {
for (const t of TRACKER_DOMAINS) {
if (hostname.includes(t.match)) return t;
}
return null;
}
function getHostname(url) {
try {
return new URL(url, location.href).hostname;
} catch (e) {
return null;
}
}
function isThirdParty(hostname) {
if (!hostname) return false;
const pageHost = location.hostname;
// crude eTLD+1 comparison
const partsA = hostname.split('.').slice(-2).join('.');
const partsB = pageHost.split('.').slice(-2).join('.');
return partsA !== partsB;
}
// -------------------------------------------------------------------
// IndexedDB storage
// -------------------------------------------------------------------
let dbPromise = null;
function getDB() {
if (dbPromise) return dbPromise;
dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(STORE_EVENTS)) {
const store = db.createObjectStore(STORE_EVENTS, { keyPath: 'id', autoIncrement: true });
store.createIndex('byDate', 'date');
store.createIndex('byDomain', 'domain');
store.createIndex('byType', 'type');
}
};
req.onsuccess = (e) => resolve(e.target.result);
req.onerror = (e) => reject(e);
});
return dbPromise;
}
let writeQueue = [];
let flushTimer = null;
function logEvent(type, data) {
const now = new Date();
const event = {
type,
domain: location.hostname,
page: location.href.split('?')[0],
date: now.toISOString().slice(0, 10),
timestamp: now.getTime(),
...data
};
writeQueue.push(event);
if (!flushTimer) {
flushTimer = setTimeout(flushQueue, 1000);
}
}
async function flushQueue() {
flushTimer = null;
if (writeQueue.length === 0) return;
const batch = writeQueue;
writeQueue = [];
try {
const db = await getDB();
const tx = db.transaction(STORE_EVENTS, 'readwrite');
const store = tx.objectStore(STORE_EVENTS);
batch.forEach(evt => store.add(evt));
} catch (e) {
console.warn('AdTech Mirror: failed to write events', e);
}
}
async function pruneOldEvents() {
try {
const db = await getDB();
const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
const tx = db.transaction(STORE_EVENTS, 'readwrite');
const store = tx.objectStore(STORE_EVENTS);
const idx = store.index('byDate');
const cutoffDate = new Date(cutoff).toISOString().slice(0, 10);
const range = IDBKeyRange.upperBound(cutoffDate, true);
const req = idx.openCursor(range);
req.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
} catch (e) {
// ignore
}
}
// -------------------------------------------------------------------
// Fingerprinting detection: Canvas
// -------------------------------------------------------------------
function hookCanvas() {
const proto = HTMLCanvasElement.prototype;
const origToDataURL = proto.toDataURL;
const origGetContext = proto.getContext;
proto.toDataURL = function (...args) {
try {
logEvent('fingerprint', {
technique: 'canvas-toDataURL',
initiator: getInitiatorScript(),
});
} catch (e) { /* noop */ }
return origToDataURL.apply(this, args);
};
const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function (...args) {
try {
logEvent('fingerprint', {
technique: 'canvas-getImageData',
initiator: getInitiatorScript(),
});
} catch (e) { /* noop */ }
return origGetImageData.apply(this, args);
};
}
// -------------------------------------------------------------------
// Fingerprinting detection: WebGL
// -------------------------------------------------------------------
function hookWebGL() {
const FINGERPRINT_PARAMS = new Set([
37445, // UNMASKED_VENDOR_WEBGL
37446, // UNMASKED_RENDERER_WEBGL
7936, // VENDOR
7937, // RENDERER
7938, // VERSION
35724, // SHADING_LANGUAGE_VERSION
]);
[WebGLRenderingContext, window.WebGL2RenderingContext].forEach(ctor => {
if (!ctor) return;
const origGetParameter = ctor.prototype.getParameter;
ctor.prototype.getParameter = function (param) {
if (FINGERPRINT_PARAMS.has(param)) {
try {
logEvent('fingerprint', {
technique: 'webgl-getParameter',
param,
initiator: getInitiatorScript(),
});
} catch (e) { /* noop */ }
}
return origGetParameter.call(this, param);
};
});
}
// -------------------------------------------------------------------
// Fingerprinting detection: AudioContext
// -------------------------------------------------------------------
function hookAudio() {
const AC = window.OfflineAudioContext || window.webkitOfflineAudioContext;
if (!AC) return;
const origStartRendering = AC.prototype.startRendering;
AC.prototype.startRendering = function (...args) {
try {
logEvent('fingerprint', {
technique: 'audio-offlineContext',
initiator: getInitiatorScript(),
});
} catch (e) { /* noop */ }
return origStartRendering.apply(this, args);
};
}
// -------------------------------------------------------------------
// Fingerprinting detection: navigator properties commonly probed
// -------------------------------------------------------------------
function hookNavigatorProps() {
const PROPS = [
'hardwareConcurrency',
'deviceMemory',
'languages',
'platform',
'userAgent',
'maxTouchPoints',
'plugins',
'productSub',
'vendor',
];
PROPS.forEach(prop => {
try {
const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, prop) ||
Object.getOwnPropertyDescriptor(window.navigator, prop);
if (!desc || !desc.get) return;
const origGetter = desc.get;
Object.defineProperty(Navigator.prototype, prop, {
...desc,
get: function () {
try {
const initiator = getInitiatorScript();
if (initiator && isThirdPartyScript(initiator)) {
logEvent('fingerprint', {
technique: 'navigator.' + prop,
initiator,
});
}
} catch (e) { /* noop */ }
return origGetter.call(this);
}
});
} catch (e) {
// some props are non-configurable in some browsers; skip
}
});
}
// -------------------------------------------------------------------
// Fingerprinting detection: Battery API (legacy but still probed)
// -------------------------------------------------------------------
function hookBattery() {
if (!navigator.getBattery) return;
const orig = navigator.getBattery;
navigator.getBattery = function (...args) {
try {
logEvent('fingerprint', {
technique: 'battery-api',
initiator: getInitiatorScript(),
});
} catch (e) { /* noop */ }
return orig.apply(this, args);
};
}
// -------------------------------------------------------------------
// Font enumeration detection (via measureText / getBoundingClientRect
// called repeatedly with many font-family changes is hard to detect
// cheaply; we approximate by counting distinct fonts measured)
// -------------------------------------------------------------------
let fontProbeCount = 0;
let fontProbeWindowStart = Date.now();
function hookFontProbing() {
const origMeasureText = CanvasRenderingContext2D.prototype.measureText;
CanvasRenderingContext2D.prototype.measureText = function (...args) {
fontProbeCount++;
const now = Date.now();
if (now - fontProbeWindowStart > 2000) {
if (fontProbeCount > 20) {
try {
logEvent('fingerprint', {
technique: 'font-enumeration',
count: fontProbeCount,
initiator: getInitiatorScript(),
});
} catch (e) { /* noop */ }
}
fontProbeCount = 0;
fontProbeWindowStart = now;
}
return origMeasureText.apply(this, args);
};
}
// -------------------------------------------------------------------
// Initiator detection: walk the stack to find the originating script URL
// -------------------------------------------------------------------
function getInitiatorScript() {
try {
const stack = new Error().stack;
if (!stack) return null;
const lines = stack.split('\n');
for (const line of lines) {
const match = line.match(/(https?:\/\/[^\s)]+):\d+:\d+/);
if (match) {
const url = match[1];
const host = getHostname(url);
if (host && host !== location.hostname) {
return url;
}
}
}
// fall back to first URL found even if first-party
for (const line of lines) {
const match = line.match(/(https?:\/\/[^\s)]+):\d+:\d+/);
if (match) return match[1];
}
} catch (e) { /* noop */ }
return null;
}
function isThirdPartyScript(url) {
const host = getHostname(url);
return isThirdParty(host);
}
// -------------------------------------------------------------------
// Third-party script / pixel / iframe detection via resource scanning
// -------------------------------------------------------------------
function scanResource(url, tagType) {
const host = getHostname(url);
if (!host) return;
const tracker = classifyDomain(host);
if (tracker) {
logEvent('tracker-load', {
trackerName: tracker.name,
category: tracker.category,
trackerDomain: host,
resourceUrl: url,
tagType,
});
} else if (isThirdParty(host)) {
logEvent('third-party-resource', {
trackerDomain: host,
resourceUrl: url,
tagType,
});
}
}
function hookResourceLoading() {
// Scan existing resources
try {
performance.getEntriesByType('resource').forEach(entry => {
scanResource(entry.name, entry.initiatorType);
});
} catch (e) { /* noop */ }
// Observe future resources
try {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
scanResource(entry.name, entry.initiatorType);
});
});
observer.observe({ entryTypes: ['resource'] });
} catch (e) { /* noop */ }
// Hook fetch
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function (input, init) {
const url = typeof input === 'string' ? input : (input && input.url);
if (url) scanResource(url, 'fetch');
return origFetch.apply(this, arguments);
};
}
// Hook XHR
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
if (url) scanResource(url, 'xhr');
return origOpen.call(this, method, url, ...rest);
};
}
// -------------------------------------------------------------------
// Cookie / localStorage syncing detection (ID-like values shared
// across third-party domains via URL params)
// -------------------------------------------------------------------
function checkForIdSync(url) {
try {
const u = new URL(url, location.href);
const host = u.hostname;
const tracker = classifyDomain(host);
if (!tracker) return;
// Look for long alphanumeric param values (likely sync IDs)
for (const [key, value] of u.searchParams.entries()) {
if (/^[a-zA-Z0-9_-]{12,}$/.test(value)) {
logEvent('id-sync', {
trackerName: tracker.name,
category: tracker.category,
trackerDomain: host,
paramName: key,
valuePreview: value.slice(0, 8) + '...',
});
break; // log one per request to avoid spam
}
}
} catch (e) { /* noop */ }
}
function hookIdSync() {
const origFetch = window.fetch;
window.fetch = function (input, init) {
const url = typeof input === 'string' ? input : (input && input.url);
if (url) checkForIdSync(url);
return origFetch.apply(this, arguments);
};
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
if (url) checkForIdSync(url);
return origOpen.call(this, method, url, ...rest);
};
}
// -------------------------------------------------------------------
// Outbound link referrer leakage
// -------------------------------------------------------------------
function hookOutboundLinks() {
document.addEventListener('click', (e) => {
let el = e.target;
while (el && el.tagName !== 'A') el = el.parentElement;
if (!el || !el.href) return;
const host = getHostname(el.href);
const tracker = classifyDomain(host);
if (tracker) {
logEvent('outbound-click', {
trackerName: tracker.name,
category: tracker.category,
trackerDomain: host,
targetUrl: el.href,
referrerLeaked: location.href,
});
}
}, true);
}
// -------------------------------------------------------------------
// Page-level summary (one entry per page visit)
// -------------------------------------------------------------------
function logPageVisit() {
logEvent('page-visit', {
title: document.title,
});
}
// -------------------------------------------------------------------
// Init
// -------------------------------------------------------------------
function init() {
try { hookCanvas(); } catch (e) { /* noop */ }
try { hookWebGL(); } catch (e) { /* noop */ }
try { hookAudio(); } catch (e) { /* noop */ }
try { hookNavigatorProps(); } catch (e) { /* noop */ }
try { hookBattery(); } catch (e) { /* noop */ }
try { hookFontProbing(); } catch (e) { /* noop */ }
try { hookIdSync(); } catch (e) { /* noop */ }
window.addEventListener('DOMContentLoaded', () => {
try { hookResourceLoading(); } catch (e) { /* noop */ }
try { hookOutboundLinks(); } catch (e) { /* noop */ }
logPageVisit();
pruneOldEvents();
});
window.addEventListener('beforeunload', flushQueue);
}
init();
// -------------------------------------------------------------------
// Dashboard
// -------------------------------------------------------------------
function injectDashboardButton() {
// Only show a small floating toggle button; full report opens in a panel.
window.addEventListener('DOMContentLoaded', () => {
const btn = document.createElement('button');
btn.textContent = '🛰️';
btn.title = 'AdTech Mirror: open report';
btn.style.cssText = `
position: fixed;
bottom: 16px;
right: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
background: #1a1a2e;
color: #fff;
border: 1px solid #444;
font-size: 18px;
cursor: pointer;
z-index: 2147483647;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
opacity: 0.5;
transition: opacity 0.2s;
`;
btn.addEventListener('mouseenter', () => btn.style.opacity = '1');
btn.addEventListener('mouseleave', () => btn.style.opacity = '0.5');
btn.addEventListener('click', () => openDashboard());
document.body.appendChild(btn);
});
}
let dashboardEl = null;
async function openDashboard() {
if (dashboardEl) {
dashboardEl.style.display = 'flex';
await renderDashboard();
return;
}
dashboardEl = document.createElement('div');
dashboardEl.id = 'adtech-mirror-dashboard';
dashboardEl.style.cssText = `
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(10, 10, 20, 0.92);
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, system-ui, sans-serif;
color: #e8e8f0;
`;
const card = document.createElement('div');
card.style.cssText = `
width: min(900px, 92vw);
height: min(720px, 90vh);
background: #16161f;
border: 1px solid #333;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
`;
const header = document.createElement('div');
header.style.cssText = `
padding: 16px 20px;
border-bottom: 1px solid #2a2a38;
display: flex;
justify-content: space-between;
align-items: center;
`;
header.innerHTML = `<div style="font-size:18px; font-weight:600;">🛰️ AdTech Mirror — Your Tracking Report</div>`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '✕';
closeBtn.style.cssText = `
background: none; border: none; color: #aaa; font-size: 18px;
cursor: pointer; padding: 4px 8px; border-radius: 4px;
`;
closeBtn.addEventListener('click', () => dashboardEl.style.display = 'none');
closeBtn.addEventListener('mouseenter', () => closeBtn.style.background = '#2a2a38');
closeBtn.addEventListener('mouseleave', () => closeBtn.style.background = 'none');
header.appendChild(closeBtn);
const body = document.createElement('div');
body.id = 'adtech-mirror-body';
body.style.cssText = `
flex-grow: 1;
overflow-y: auto;
padding: 20px;
`;
const footer = document.createElement('div');
footer.style.cssText = `
padding: 12px 20px;
border-top: 1px solid #2a2a38;
display: flex;
gap: 10px;
justify-content: flex-end;
`;
const exportBtn = makeButton('Export JSON', exportData);
const exportCsvBtn = makeButton('Export CSV', exportCSV);
const clearBtn = makeButton('Clear All Data', clearAllData, true);
footer.appendChild(exportBtn);
footer.appendChild(exportCsvBtn);
footer.appendChild(clearBtn);
card.appendChild(header);
card.appendChild(body);
card.appendChild(footer);
dashboardEl.appendChild(card);
document.body.appendChild(dashboardEl);
dashboardEl.addEventListener('click', (e) => {
if (e.target === dashboardEl) dashboardEl.style.display = 'none';
});
await renderDashboard();
}
function makeButton(label, onClick, danger) {
const btn = document.createElement('button');
btn.textContent = label;
btn.style.cssText = `
padding: 8px 14px;
border-radius: 6px;
border: 1px solid ${danger ? '#5a2a2a' : '#333'};
background: ${danger ? '#2a1414' : '#222230'};
color: ${danger ? '#ff8a8a' : '#e8e8f0'};
cursor: pointer;
font-size: 13px;
`;
btn.addEventListener('click', onClick);
return btn;
}
async function getAllEvents() {
const db = await getDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_EVENTS, 'readonly');
const store = tx.objectStore(STORE_EVENTS);
const req = store.getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = (e) => reject(e);
});
}
async function renderDashboard() {
const body = document.getElementById('adtech-mirror-body');
if (!body) return;
body.innerHTML = '<div style="color:#888;">Loading…</div>';
await flushQueue();
const events = await getAllEvents();
if (events.length === 0) {
body.innerHTML = `
<div style="text-align:center; color:#888; margin-top: 60px;">
<div style="font-size:40px; margin-bottom:12px;">👀</div>
<div>No data collected yet. Browse normally for a while, then check back.</div>
</div>`;
return;
}
const stats = computeStats(events);
body.innerHTML = `
${renderSummaryCards(stats)}
${renderTopTrackers(stats)}
${renderFingerprintBreakdown(stats)}
${renderIdSyncSection(stats)}
${renderSyntheticProfile(stats)}
${renderTimeline(stats)}
`;
}
function computeStats(events) {
const days = new Set();
const sitesVisited = new Set();
const trackerCounts = {}; // domain -> {name, category, count, sites:Set}
const fingerprintCounts = {}; // technique -> count
const idSyncs = []; // list
const categoryTotals = {};
const trackersBySite = {}; // site -> Set(trackerName)
events.forEach(evt => {
days.add(evt.date);
sitesVisited.add(evt.domain);
if (evt.type === 'tracker-load' || evt.type === 'outbound-click' || evt.type === 'id-sync') {
const key = evt.trackerDomain;
if (!trackerCounts[key]) {
trackerCounts[key] = { name: evt.trackerName, category: evt.category, count: 0, sites: new Set() };
}
trackerCounts[key].count++;
trackerCounts[key].sites.add(evt.domain);
categoryTotals[evt.category] = (categoryTotals[evt.category] || 0) + 1;
if (!trackersBySite[evt.domain]) trackersBySite[evt.domain] = new Set();
trackersBySite[evt.domain].add(evt.trackerName);
}
if (evt.type === 'fingerprint') {
fingerprintCounts[evt.technique] = (fingerprintCounts[evt.technique] || 0) + 1;
}
if (evt.type === 'id-sync') {
idSyncs.push(evt);
}
});
// Cross-site identity linking: trackers seen on 2+ sites
const crossSiteTrackers = Object.entries(trackerCounts)
.filter(([_, v]) => v.sites.size >= 2)
.sort((a, b) => b[1].sites.size - a[1].sites.size);
return {
totalEvents: events.length,
daysActive: days.size,
sitesVisited: sitesVisited.size,
trackerCounts,
fingerprintCounts,
idSyncs,
categoryTotals,
crossSiteTrackers,
events,
};
}
function renderSummaryCards(stats) {
const totalFingerprints = Object.values(stats.fingerprintCounts).reduce((a, b) => a + b, 0);
const totalTrackers = Object.keys(stats.trackerCounts).length;
const cards = [
{ label: 'Sites Visited', value: stats.sitesVisited },
{ label: 'Days Logged', value: stats.daysActive },
{ label: 'Unique Trackers Seen', value: totalTrackers },
{ label: 'Fingerprint Attempts', value: totalFingerprints },
{ label: 'ID Sync Events', value: stats.idSyncs.length },
];
return `
<div style="display:flex; gap:12px; margin-bottom:24px; flex-wrap:wrap;">
${cards.map(c => `
<div style="flex:1; min-width:120px; background:#1e1e2a; border:1px solid #2a2a38; border-radius:8px; padding:14px;">
<div style="font-size:24px; font-weight:700;">${c.value}</div>
<div style="font-size:12px; color:#999; margin-top:4px;">${c.label}</div>
</div>
`).join('')}
</div>
`;
}
function renderTopTrackers(stats) {
const sorted = Object.entries(stats.trackerCounts)
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 15);
if (sorted.length === 0) return '';
const rows = sorted.map(([domain, data]) => `
<tr style="border-bottom:1px solid #2a2a38;">
<td style="padding:8px 6px;">${escapeHtml(data.name || domain)}</td>
<td style="padding:8px 6px; color:#999;">${escapeHtml(data.category || '')}</td>
<td style="padding:8px 6px; text-align:right;">${data.sites.size}</td>
<td style="padding:8px 6px; text-align:right;">${data.count}</td>
</tr>
`).join('');
return `
<div style="margin-bottom:24px;">
<h3 style="font-size:14px; color:#bbb; margin-bottom:8px;">Top Trackers Observed</h3>
<table style="width:100%; border-collapse:collapse; font-size:13px;">
<thead>
<tr style="text-align:left; color:#777; border-bottom:1px solid #2a2a38;">
<th style="padding:6px;">Tracker</th>
<th style="padding:6px;">Category</th>
<th style="padding:6px; text-align:right;">Sites</th>
<th style="padding:6px; text-align:right;">Hits</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
function renderFingerprintBreakdown(stats) {
const entries = Object.entries(stats.fingerprintCounts).sort((a, b) => b[1] - a[1]);
if (entries.length === 0) return '';
const TECHNIQUE_LABELS = {
'canvas-toDataURL': 'Canvas Fingerprinting (toDataURL)',
'canvas-getImageData': 'Canvas Fingerprinting (pixel read)',
'webgl-getParameter': 'WebGL/GPU Fingerprinting',
'audio-offlineContext': 'Audio Fingerprinting',
'battery-api': 'Battery Status Probing',
'font-enumeration': 'Font Enumeration',
};
const max = Math.max(...entries.map(e => e[1]));
const rows = entries.map(([technique, count]) => {
const label = TECHNIQUE_LABELS[technique] || technique;
const pct = Math.round((count / max) * 100);
return `
<div style="margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:2px;">
<span>${escapeHtml(label)}</span>
<span style="color:#999;">${count}</span>
</div>
<div style="background:#2a2a38; border-radius:4px; height:6px;">
<div style="background:#6c5ce7; height:6px; border-radius:4px; width:${pct}%;"></div>
</div>
</div>
`;
}).join('');
return `
<div style="margin-bottom:24px;">
<h3 style="font-size:14px; color:#bbb; margin-bottom:8px;">Fingerprinting Techniques Detected</h3>
${rows}
</div>
`;
}
function renderIdSyncSection(stats) {
if (stats.crossSiteTrackers.length === 0) return '';
const rows = stats.crossSiteTrackers.slice(0, 10).map(([domain, data]) => `
<tr style="border-bottom:1px solid #2a2a38;">
<td style="padding:8px 6px;">${escapeHtml(data.name || domain)}</td>
<td style="padding:8px 6px; text-align:right;">${data.sites.size}</td>
<td style="padding:8px 6px; color:#999; font-size:12px;">${Array.from(data.sites).slice(0, 4).map(escapeHtml).join(', ')}${data.sites.size > 4 ? '…' : ''}</td>
</tr>
`).join('');
return `
<div style="margin-bottom:24px;">
<h3 style="font-size:14px; color:#bbb; margin-bottom:8px;">Cross-Site Identity Linking</h3>
<p style="font-size:12px; color:#888; margin-bottom:8px;">
These trackers appeared on multiple different sites you visited — meaning they can potentially
link your activity across those sites into a single profile.
</p>
<table style="width:100%; border-collapse:collapse; font-size:13px;">
<thead>
<tr style="text-align:left; color:#777; border-bottom:1px solid #2a2a38;">
<th style="padding:6px;">Tracker</th>
<th style="padding:6px; text-align:right;">Sites Linked</th>
<th style="padding:6px;">Examples</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
function renderSyntheticProfile(stats) {
const topCategories = Object.entries(stats.categoryTotals)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([cat]) => CATEGORY_LABELS[cat] || cat);
const totalFingerprints = Object.values(stats.fingerprintCounts).reduce((a, b) => a + b, 0);
const linkedSites = stats.crossSiteTrackers.length > 0
? stats.crossSiteTrackers[0][1].sites.size
: 0;
const lines = [];
lines.push(`Over <b>${stats.daysActive} day${stats.daysActive === 1 ? '' : 's'}</b> of browsing across <b>${stats.sitesVisited} sites</b>, this browser was exposed to <b>${Object.keys(stats.trackerCounts).length} distinct tracking entities</b>.`);
if (topCategories.length) {
lines.push(`The most active categories were: <b>${topCategories.join(', ')}</b>.`);
}
if (totalFingerprints > 0) {
lines.push(`Scripts attempted device fingerprinting <b>${totalFingerprints} times</b> — these techniques (canvas, WebGL, audio, fonts) can re-identify this browser even if cookies are cleared.`);
}
if (linkedSites >= 2) {
const topName = stats.crossSiteTrackers[0][1].name;
lines.push(`At least one tracker (<b>${escapeHtml(topName)}</b>) appeared on <b>${linkedSites} different sites</b>, meaning it can likely connect browsing activity across those sites to the same profile.`);
}
if (stats.idSyncs.length > 0) {
lines.push(`<b>${stats.idSyncs.length} cookie/ID-sync events</b> were observed — these are requests where tracking IDs are shared between ad companies behind the scenes.`);
}
return `
<div style="margin-bottom:24px; background:#1e1e2a; border:1px solid #2a2a38; border-radius:8px; padding:16px;">
<h3 style="font-size:14px; color:#bbb; margin-bottom:8px;">Your Synthetic Profile (Plain Language)</h3>
<p style="font-size:13px; line-height:1.6; color:#ddd;">${lines.join(' ')}</p>
</div>
`;
}
const CATEGORY_LABELS = {
'ad-exchange': 'Ad Exchanges',
'ad-network': 'Ad Networks',
'pixel': 'Conversion Pixels',
'ssp': 'Supply-Side Platforms',
'dsp': 'Demand-Side Platforms',
'dmp': 'Data Management Platforms',
'cdp': 'Customer Data Platforms',
'analytics': 'Analytics & Session Recording',
'identity': 'Identity Resolution',
'data-broker': 'Data Brokers',
'fingerprint-vendor': 'Fingerprinting Vendors',
'tag-manager': 'Tag Managers',
};
function renderTimeline(stats) {
const byDate = {};
stats.events.forEach(evt => {
byDate[evt.date] = (byDate[evt.date] || 0) + 1;
});
const dates = Object.keys(byDate).sort();
if (dates.length === 0) return '';
const max = Math.max(...Object.values(byDate));
const bars = dates.map(date => {
const h = Math.max(4, Math.round((byDate[date] / max) * 60));
return `
<div style="display:flex; flex-direction:column; align-items:center; gap:4px;">
<div title="${date}: ${byDate[date]} events" style="width:10px; height:${h}px; background:#6c5ce7; border-radius:2px;"></div>
</div>
`;
}).join('');
return `
<div style="margin-bottom:8px;">
<h3 style="font-size:14px; color:#bbb; margin-bottom:8px;">Activity Timeline</h3>
<div style="display:flex; align-items:flex-end; gap:3px; height:64px; overflow-x:auto; padding-bottom:4px;">
${bars}
</div>
<div style="font-size:11px; color:#777; margin-top:4px;">${dates[0]} → ${dates[dates.length - 1]}</div>
</div>
`;
}
function escapeHtml(str) {
if (str == null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
async function exportData() {
await flushQueue();
const events = await getAllEvents();
const blob = new Blob([JSON.stringify(events, null, 2)], { type: 'application/json' });
downloadBlob(blob, 'adtech-mirror-export.json');
}
async function exportCSV() {
await flushQueue();
const events = await getAllEvents();
if (events.length === 0) return;
const keys = Array.from(new Set(events.flatMap(e => Object.keys(e))));
const rows = [keys.join(',')];
events.forEach(evt => {
rows.push(keys.map(k => {
const v = evt[k];
if (v == null) return '';
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
return '"' + s.replace(/"/g, '""') + '"';
}).join(','));
});
const blob = new Blob([rows.join('\n')], { type: 'text/csv' });
downloadBlob(blob, 'adtech-mirror-export.csv');
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
async function clearAllData() {
if (!confirm('This will permanently delete all AdTech Mirror logged data. Continue?')) return;
const db = await getDB();
const tx = db.transaction(STORE_EVENTS, 'readwrite');
tx.objectStore(STORE_EVENTS).clear();
tx.oncomplete = () => renderDashboard();
}
injectDashboardButton();
})();