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.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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

})();