AdTech Mirror

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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    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();

})();