Removes Google result redirect wrappers, click tracking, referrers, AMP wrappers, third-party tracking params, and telemetry beacons; with optional forced SafeSearch disable, an in-page settings panel, and console diagnostics to confirm the script is actually injected.
// ==UserScript==
// @name Don't Track Me Google
// @namespace https://github.com/rob--w/dont-track-me-google
// @version 2.2.2
// @description Removes Google result redirect wrappers, click tracking, referrers, AMP wrappers, third-party tracking params, and telemetry beacons; with optional forced SafeSearch disable, an in-page settings panel, and console diagnostics to confirm the script is actually injected.
// @author KTZ
// @license MIT
// The two lines below ("google.*" / "*.google.*") are NOT valid
// userscript-manager match patterns: @match only allows a wildcard as
// the whole host or as a "*." subdomain prefix, never at the end of a
// domain (Tampermonkey/Violentmonkey/Greasemonkey all reject or
// silently no-op a trailing "google.*"). That means the script only
// ever actually ran on the literal host "google.*", which doesn't
// exist - so, ironically, it likely wasn't matching real Google pages
// at all in most managers. Fixed by explicitly listing every Google
// country-code TLD (source: Chromium's GOOGLE_TLD_LIST) so the script
// runs on google.com, google.co.uk, google.de, ... and all of their
// subdomains (www., maps., consent., etc.).
// @match *://google.ac/*
// @match *://*.google.ac/*
// @match *://google.ad/*
// @match *://*.google.ad/*
// @match *://google.ae/*
// @match *://*.google.ae/*
// @match *://google.af/*
// @match *://*.google.af/*
// @match *://google.ag/*
// @match *://*.google.ag/*
// @match *://google.al/*
// @match *://*.google.al/*
// @match *://google.am/*
// @match *://*.google.am/*
// @match *://google.as/*
// @match *://*.google.as/*
// @match *://google.at/*
// @match *://*.google.at/*
// @match *://google.aw/*
// @match *://*.google.aw/*
// @match *://google.az/*
// @match *://*.google.az/*
// @match *://google.ba/*
// @match *://*.google.ba/*
// @match *://google.be/*
// @match *://*.google.be/*
// @match *://google.bf/*
// @match *://*.google.bf/*
// @match *://google.bg/*
// @match *://*.google.bg/*
// @match *://google.bi/*
// @match *://*.google.bi/*
// @match *://google.biz/*
// @match *://*.google.biz/*
// @match *://google.bj/*
// @match *://*.google.bj/*
// @match *://google.bm/*
// @match *://*.google.bm/*
// @match *://google.bn/*
// @match *://*.google.bn/*
// @match *://google.bo/*
// @match *://*.google.bo/*
// @match *://google.bs/*
// @match *://*.google.bs/*
// @match *://google.bt/*
// @match *://*.google.bt/*
// @match *://google.by/*
// @match *://*.google.by/*
// @match *://google.ca/*
// @match *://*.google.ca/*
// @match *://google.cat/*
// @match *://*.google.cat/*
// @match *://google.cc/*
// @match *://*.google.cc/*
// @match *://google.cd/*
// @match *://*.google.cd/*
// @match *://google.cf/*
// @match *://*.google.cf/*
// @match *://google.cg/*
// @match *://*.google.cg/*
// @match *://google.ch/*
// @match *://*.google.ch/*
// @match *://google.ci/*
// @match *://*.google.ci/*
// @match *://google.cl/*
// @match *://*.google.cl/*
// @match *://google.cm/*
// @match *://*.google.cm/*
// @match *://google.cn/*
// @match *://*.google.cn/*
// @match *://google.co/*
// @match *://*.google.co/*
// @match *://google.co.ao/*
// @match *://*.google.co.ao/*
// @match *://google.co.at/*
// @match *://*.google.co.at/*
// @match *://google.co.bw/*
// @match *://*.google.co.bw/*
// @match *://google.co.ck/*
// @match *://*.google.co.ck/*
// @match *://google.co.cr/*
// @match *://*.google.co.cr/*
// @match *://google.co.gg/*
// @match *://*.google.co.gg/*
// @match *://google.co.gy/*
// @match *://*.google.co.gy/*
// @match *://google.co.hu/*
// @match *://*.google.co.hu/*
// @match *://google.co.id/*
// @match *://*.google.co.id/*
// @match *://google.co.il/*
// @match *://*.google.co.il/*
// @match *://google.co.im/*
// @match *://*.google.co.im/*
// @match *://google.co.in/*
// @match *://*.google.co.in/*
// @match *://google.co.je/*
// @match *://*.google.co.je/*
// @match *://google.co.jp/*
// @match *://*.google.co.jp/*
// @match *://google.co.ke/*
// @match *://*.google.co.ke/*
// @match *://google.co.kr/*
// @match *://*.google.co.kr/*
// @match *://google.co.ls/*
// @match *://*.google.co.ls/*
// @match *://google.co.ma/*
// @match *://*.google.co.ma/*
// @match *://google.co.mz/*
// @match *://*.google.co.mz/*
// @match *://google.co.nz/*
// @match *://*.google.co.nz/*
// @match *://google.co.rs/*
// @match *://*.google.co.rs/*
// @match *://google.co.th/*
// @match *://*.google.co.th/*
// @match *://google.co.tz/*
// @match *://*.google.co.tz/*
// @match *://google.co.ua/*
// @match *://*.google.co.ua/*
// @match *://google.co.ug/*
// @match *://*.google.co.ug/*
// @match *://google.co.uk/*
// @match *://*.google.co.uk/*
// @match *://google.co.uz/*
// @match *://*.google.co.uz/*
// @match *://google.co.ve/*
// @match *://*.google.co.ve/*
// @match *://google.co.vi/*
// @match *://*.google.co.vi/*
// @match *://google.co.za/*
// @match *://*.google.co.za/*
// @match *://google.co.zm/*
// @match *://*.google.co.zm/*
// @match *://google.co.zw/*
// @match *://*.google.co.zw/*
// @match *://google.com/*
// @match *://*.google.com/*
// @match *://google.com.af/*
// @match *://*.google.com.af/*
// @match *://google.com.ag/*
// @match *://*.google.com.ag/*
// @match *://google.com.ai/*
// @match *://*.google.com.ai/*
// @match *://google.com.ar/*
// @match *://*.google.com.ar/*
// @match *://google.com.au/*
// @match *://*.google.com.au/*
// @match *://google.com.az/*
// @match *://*.google.com.az/*
// @match *://google.com.bd/*
// @match *://*.google.com.bd/*
// @match *://google.com.bh/*
// @match *://*.google.com.bh/*
// @match *://google.com.bi/*
// @match *://*.google.com.bi/*
// @match *://google.com.bn/*
// @match *://*.google.com.bn/*
// @match *://google.com.bo/*
// @match *://*.google.com.bo/*
// @match *://google.com.br/*
// @match *://*.google.com.br/*
// @match *://google.com.by/*
// @match *://*.google.com.by/*
// @match *://google.com.bz/*
// @match *://*.google.com.bz/*
// @match *://google.com.cn/*
// @match *://*.google.com.cn/*
// @match *://google.com.co/*
// @match *://*.google.com.co/*
// @match *://google.com.cu/*
// @match *://*.google.com.cu/*
// @match *://google.com.cy/*
// @match *://*.google.com.cy/*
// @match *://google.com.do/*
// @match *://*.google.com.do/*
// @match *://google.com.dz/*
// @match *://*.google.com.dz/*
// @match *://google.com.ec/*
// @match *://*.google.com.ec/*
// @match *://google.com.eg/*
// @match *://*.google.com.eg/*
// @match *://google.com.er/*
// @match *://*.google.com.er/*
// @match *://google.com.et/*
// @match *://*.google.com.et/*
// @match *://google.com.fj/*
// @match *://*.google.com.fj/*
// @match *://google.com.ge/*
// @match *://*.google.com.ge/*
// @match *://google.com.gh/*
// @match *://*.google.com.gh/*
// @match *://google.com.gi/*
// @match *://*.google.com.gi/*
// @match *://google.com.gp/*
// @match *://*.google.com.gp/*
// @match *://google.com.gr/*
// @match *://*.google.com.gr/*
// @match *://google.com.gt/*
// @match *://*.google.com.gt/*
// @match *://google.com.gy/*
// @match *://*.google.com.gy/*
// @match *://google.com.hk/*
// @match *://*.google.com.hk/*
// @match *://google.com.ht/*
// @match *://*.google.com.ht/*
// @match *://google.com.iq/*
// @match *://*.google.com.iq/*
// @match *://google.com.jm/*
// @match *://*.google.com.jm/*
// @match *://google.com.jo/*
// @match *://*.google.com.jo/*
// @match *://google.com.kh/*
// @match *://*.google.com.kh/*
// @match *://google.com.kw/*
// @match *://*.google.com.kw/*
// @match *://google.com.kz/*
// @match *://*.google.com.kz/*
// @match *://google.com.lb/*
// @match *://*.google.com.lb/*
// @match *://google.com.lv/*
// @match *://*.google.com.lv/*
// @match *://google.com.ly/*
// @match *://*.google.com.ly/*
// @match *://google.com.mm/*
// @match *://*.google.com.mm/*
// @match *://google.com.mt/*
// @match *://*.google.com.mt/*
// @match *://google.com.mx/*
// @match *://*.google.com.mx/*
// @match *://google.com.my/*
// @match *://*.google.com.my/*
// @match *://google.com.na/*
// @match *://*.google.com.na/*
// @match *://google.com.nc/*
// @match *://*.google.com.nc/*
// @match *://google.com.nf/*
// @match *://*.google.com.nf/*
// @match *://google.com.ng/*
// @match *://*.google.com.ng/*
// @match *://google.com.ni/*
// @match *://*.google.com.ni/*
// @match *://google.com.np/*
// @match *://*.google.com.np/*
// @match *://google.com.nr/*
// @match *://*.google.com.nr/*
// @match *://google.com.om/*
// @match *://*.google.com.om/*
// @match *://google.com.pa/*
// @match *://*.google.com.pa/*
// @match *://google.com.pe/*
// @match *://*.google.com.pe/*
// @match *://google.com.pg/*
// @match *://*.google.com.pg/*
// @match *://google.com.ph/*
// @match *://*.google.com.ph/*
// @match *://google.com.pk/*
// @match *://*.google.com.pk/*
// @match *://google.com.pl/*
// @match *://*.google.com.pl/*
// @match *://google.com.pr/*
// @match *://*.google.com.pr/*
// @match *://google.com.ps/*
// @match *://*.google.com.ps/*
// @match *://google.com.pt/*
// @match *://*.google.com.pt/*
// @match *://google.com.py/*
// @match *://*.google.com.py/*
// @match *://google.com.qa/*
// @match *://*.google.com.qa/*
// @match *://google.com.ru/*
// @match *://*.google.com.ru/*
// @match *://google.com.sa/*
// @match *://*.google.com.sa/*
// @match *://google.com.sb/*
// @match *://*.google.com.sb/*
// @match *://google.com.sg/*
// @match *://*.google.com.sg/*
// @match *://google.com.sl/*
// @match *://*.google.com.sl/*
// @match *://google.com.sv/*
// @match *://*.google.com.sv/*
// @match *://google.com.tj/*
// @match *://*.google.com.tj/*
// @match *://google.com.tm/*
// @match *://*.google.com.tm/*
// @match *://google.com.tn/*
// @match *://*.google.com.tn/*
// @match *://google.com.tr/*
// @match *://*.google.com.tr/*
// @match *://google.com.tw/*
// @match *://*.google.com.tw/*
// @match *://google.com.ua/*
// @match *://*.google.com.ua/*
// @match *://google.com.uy/*
// @match *://*.google.com.uy/*
// @match *://google.com.vc/*
// @match *://*.google.com.vc/*
// @match *://google.com.ve/*
// @match *://*.google.com.ve/*
// @match *://google.com.vn/*
// @match *://*.google.com.vn/*
// @match *://google.cv/*
// @match *://*.google.cv/*
// @match *://google.cz/*
// @match *://*.google.cz/*
// @match *://google.de/*
// @match *://*.google.de/*
// @match *://google.dj/*
// @match *://*.google.dj/*
// @match *://google.dk/*
// @match *://*.google.dk/*
// @match *://google.dm/*
// @match *://*.google.dm/*
// @match *://google.do/*
// @match *://*.google.do/*
// @match *://google.dz/*
// @match *://*.google.dz/*
// @match *://google.ec/*
// @match *://*.google.ec/*
// @match *://google.ee/*
// @match *://*.google.ee/*
// @match *://google.es/*
// @match *://*.google.es/*
// @match *://google.eu/*
// @match *://*.google.eu/*
// @match *://google.fi/*
// @match *://*.google.fi/*
// @match *://google.fm/*
// @match *://*.google.fm/*
// @match *://google.fr/*
// @match *://*.google.fr/*
// @match *://google.ga/*
// @match *://*.google.ga/*
// @match *://google.gd/*
// @match *://*.google.gd/*
// @match *://google.ge/*
// @match *://*.google.ge/*
// @match *://google.gf/*
// @match *://*.google.gf/*
// @match *://google.gg/*
// @match *://*.google.gg/*
// @match *://google.gl/*
// @match *://*.google.gl/*
// @match *://google.gm/*
// @match *://*.google.gm/*
// @match *://google.gp/*
// @match *://*.google.gp/*
// @match *://google.gr/*
// @match *://*.google.gr/*
// @match *://google.gw/*
// @match *://*.google.gw/*
// @match *://google.gy/*
// @match *://*.google.gy/*
// @match *://google.hk/*
// @match *://*.google.hk/*
// @match *://google.hn/*
// @match *://*.google.hn/*
// @match *://google.hr/*
// @match *://*.google.hr/*
// @match *://google.ht/*
// @match *://*.google.ht/*
// @match *://google.hu/*
// @match *://*.google.hu/*
// @match *://google.ie/*
// @match *://*.google.ie/*
// @match *://google.im/*
// @match *://*.google.im/*
// @match *://google.in/*
// @match *://*.google.in/*
// @match *://google.info/*
// @match *://*.google.info/*
// @match *://google.io/*
// @match *://*.google.io/*
// @match *://google.iq/*
// @match *://*.google.iq/*
// @match *://google.is/*
// @match *://*.google.is/*
// @match *://google.it/*
// @match *://*.google.it/*
// @match *://google.it.ao/*
// @match *://*.google.it.ao/*
// @match *://google.je/*
// @match *://*.google.je/*
// @match *://google.jo/*
// @match *://*.google.jo/*
// @match *://google.jobs/*
// @match *://*.google.jobs/*
// @match *://google.jp/*
// @match *://*.google.jp/*
// @match *://google.kg/*
// @match *://*.google.kg/*
// @match *://google.ki/*
// @match *://*.google.ki/*
// @match *://google.km/*
// @match *://*.google.km/*
// @match *://google.kr/*
// @match *://*.google.kr/*
// @match *://google.kz/*
// @match *://*.google.kz/*
// @match *://google.la/*
// @match *://*.google.la/*
// @match *://google.li/*
// @match *://*.google.li/*
// @match *://google.lk/*
// @match *://*.google.lk/*
// @match *://google.lt/*
// @match *://*.google.lt/*
// @match *://google.lu/*
// @match *://*.google.lu/*
// @match *://google.lv/*
// @match *://*.google.lv/*
// @match *://google.ma/*
// @match *://*.google.ma/*
// @match *://google.md/*
// @match *://*.google.md/*
// @match *://google.me/*
// @match *://*.google.me/*
// @match *://google.mg/*
// @match *://*.google.mg/*
// @match *://google.mh/*
// @match *://*.google.mh/*
// @match *://google.mk/*
// @match *://*.google.mk/*
// @match *://google.ml/*
// @match *://*.google.ml/*
// @match *://google.mn/*
// @match *://*.google.mn/*
// @match *://google.mr/*
// @match *://*.google.mr/*
// @match *://google.ms/*
// @match *://*.google.ms/*
// @match *://google.mu/*
// @match *://*.google.mu/*
// @match *://google.mv/*
// @match *://*.google.mv/*
// @match *://google.mw/*
// @match *://*.google.mw/*
// @match *://google.mx/*
// @match *://*.google.mx/*
// @match *://google.name/*
// @match *://*.google.name/*
// @match *://google.ne/*
// @match *://*.google.ne/*
// @match *://google.ne.jp/*
// @match *://*.google.ne.jp/*
// @match *://google.net/*
// @match *://*.google.net/*
// @match *://google.net.nz/*
// @match *://*.google.net.nz/*
// @match *://google.ng/*
// @match *://*.google.ng/*
// @match *://google.nl/*
// @match *://*.google.nl/*
// @match *://google.no/*
// @match *://*.google.no/*
// @match *://google.nom.es/*
// @match *://*.google.nom.es/*
// @match *://google.nr/*
// @match *://*.google.nr/*
// @match *://google.nu/*
// @match *://*.google.nu/*
// @match *://google.off.ai/*
// @match *://*.google.off.ai/*
// @match *://google.org/*
// @match *://*.google.org/*
// @match *://google.org.es/*
// @match *://*.google.org.es/*
// @match *://google.org.nz/*
// @match *://*.google.org.nz/*
// @match *://google.org.uk/*
// @match *://*.google.org.uk/*
// @match *://google.pf/*
// @match *://*.google.pf/*
// @match *://google.ph/*
// @match *://*.google.ph/*
// @match *://google.pk/*
// @match *://*.google.pk/*
// @match *://google.pl/*
// @match *://*.google.pl/*
// @match *://google.pn/*
// @match *://*.google.pn/*
// @match *://google.pro/*
// @match *://*.google.pro/*
// @match *://google.ps/*
// @match *://*.google.ps/*
// @match *://google.pt/*
// @match *://*.google.pt/*
// @match *://google.qa/*
// @match *://*.google.qa/*
// @match *://google.re/*
// @match *://*.google.re/*
// @match *://google.ro/*
// @match *://*.google.ro/*
// @match *://google.rs/*
// @match *://*.google.rs/*
// @match *://google.ru/*
// @match *://*.google.ru/*
// @match *://google.rw/*
// @match *://*.google.rw/*
// @match *://google.sc/*
// @match *://*.google.sc/*
// @match *://google.se/*
// @match *://*.google.se/*
// @match *://google.sg/*
// @match *://*.google.sg/*
// @match *://google.sh/*
// @match *://*.google.sh/*
// @match *://google.si/*
// @match *://*.google.si/*
// @match *://google.sk/*
// @match *://*.google.sk/*
// @match *://google.sl/*
// @match *://*.google.sl/*
// @match *://google.sm/*
// @match *://*.google.sm/*
// @match *://google.sn/*
// @match *://*.google.sn/*
// @match *://google.so/*
// @match *://*.google.so/*
// @match *://google.sr/*
// @match *://*.google.sr/*
// @match *://google.st/*
// @match *://*.google.st/*
// @match *://google.sz/*
// @match *://*.google.sz/*
// @match *://google.td/*
// @match *://*.google.td/*
// @match *://google.tel/*
// @match *://*.google.tel/*
// @match *://google.tg/*
// @match *://*.google.tg/*
// @match *://google.tk/*
// @match *://*.google.tk/*
// @match *://google.tl/*
// @match *://*.google.tl/*
// @match *://google.tm/*
// @match *://*.google.tm/*
// @match *://google.tn/*
// @match *://*.google.tn/*
// @match *://google.to/*
// @match *://*.google.to/*
// @match *://google.tt/*
// @match *://*.google.tt/*
// @match *://google.tv/*
// @match *://*.google.tv/*
// @match *://google.tw/*
// @match *://*.google.tw/*
// @match *://google.ua/*
// @match *://*.google.ua/*
// @match *://google.us/*
// @match *://*.google.us/*
// @match *://google.uz/*
// @match *://*.google.uz/*
// @match *://google.vg/*
// @match *://*.google.vg/*
// @match *://google.vn/*
// @match *://*.google.vn/*
// @match *://google.vu/*
// @match *://*.google.vu/*
// @match *://google.ws/*
// @match *://*.google.ws/*
// @match *://google.yt/*
// @match *://*.google.yt/*
// @run-at document-start
// @noframes
// ==/UserScript==
(function () {
'use strict';
/*
* Don't Track Me Google 2.2.0
*
* Design goals:
* - No mandatory userscript-manager API (works with an implicit
* "@grant none" so the script always runs directly in the page's
* real window/document instead of a sandboxed proxy - this is
* required for the network-level guards below to actually see and
* patch the real fetch/XHR/sendBeacon/Image/anchor objects).
* - Works with Tampermonkey, Violentmonkey, Greasemonkey and other
* userscript managers.
* - Safe localStorage fallback when GM_* APIs are unavailable.
* - In-page floating settings panel as a fallback UI for managers
* that don't expose GM_registerMenuCommand at all (common on
* several mobile userscript apps).
* - Cleans links before Google can process most click handlers, and
* guards against later JS rewrites (e.g. Google Maps setting
* `a.href` during event bubbling).
* - Handles dynamically inserted results, SPA navigation, and open
* shadow DOM subtrees.
* - Unwraps AMP viewer URLs and strips common third-party tracking
* parameters (utm_*, fbclid, msclkid, ...) from destination links.
* - Blocks known Google/ad-network telemetry beacons sent via
* sendBeacon, fetch, XHR, or tracking-pixel <img> requests.
* - Forces Google SafeSearch off when enabled.
* - Does not globally block normal Google navigation.
*
* Changelog:
* 2.2.2
* - Fixed @match: the old "google-dot-star" and
* "star-dot-google-dot-star" patterns are not valid userscript
* match patterns (a wildcard can only be the whole host or a
* leading subdomain wildcard, never a trailing "domain-dot-star"),
* so the script was likely not actually being injected on real
* Google pages in most managers. Replaced with an explicit
* subdomain-wildcard match entry for every Google country-code
* TLD, which also matches the bare domain and any subdomain
* (www., maps., consent., images., ...).
* 2.2.1
* - Added an unconditional startup banner (independent of the
* `debug` setting) so it's possible to confirm from the console
* alone whether the script file is being injected at all.
* - Wrapped every independent subsystem (menu, guards, observer,
* SPA watcher, safety sweep, panel) in its own error boundary so
* one failing feature can no longer silently prevent the rest -
* including basic link cleaning - from running.
* - Added an initialized guard so init() can't run twice if both
* DOMContentLoaded and load fire.
* - Exposed startup errors via DontTrackMeGoogle.getStartupErrors()
* for easier troubleshooting.
* 2.2.0
* - Added AMP link unwrapping (unwrapAmpLinks).
* - Added destination tracking-parameter stripping for non-Google
* links (stripDestinationTrackingParams).
* - Added telemetry-beacon blocking across sendBeacon, fetch, XHR
* and tracking-pixel <img> requests (blockTrackingBeacons).
* - Added tracking-pixel scrubbing for pixels already in the DOM
* (removeTrackingPixels).
* - Added an aggressive `a.href` property-setter guard so late,
* script-driven href rewrites are cleaned too
* (aggressiveHrefGuard).
* - Added open shadow-DOM traversal and observation
* (cleanShadowDom).
* - Added data-ved / data-jsarwt / data-usg attribute scrubbing
* (removeTrackingAttributes).
* - Added consent.google.com and translate.google.com redirect
* unwrapping.
* - Added an in-page floating settings panel that works even
* without GM_registerMenuCommand support.
* - Added a lightweight stats counter exposed via the diagnostic
* API.
* 2.1.1
* - Baseline release (redirect unwrapping, ping/inline-tracking
* removal, no-referrer, optional tracking-param stripping,
* forced SafeSearch off, MutationObserver + SPA handling).
*/
const VERSION = '2.2.2';
const PREFIX = '[DontTrackMeGoogle]';
const STORAGE_KEY = 'DTMG:settings';
const CLEAN_MARK = 'data-dtmg-cleaned';
/*
* Unconditional startup banner (independent of the `debug` setting).
* If this line never appears in the console on a Google page, the
* script itself is not being injected at all - that points to the
* userscript manager/browser (e.g. Chrome's "Allow User Scripts"
* permission, the script being disabled, or a stale/duplicate
* install) rather than a bug in this file. See the "Troubleshooting"
* section in the script's description for details.
*/
try {
console.info(`${PREFIX} v${VERSION} script file loaded on ${location.hostname}`);
} catch (_) {
// Ignore environments without a console.
}
const STARTUP_ERRORS = [];
function safeCall(label, fn) {
try {
fn();
return true;
} catch (e) {
STARTUP_ERRORS.push({ label, error: e });
try {
console.error(`${PREFIX} "${label}" failed - continuing without it:`, e);
} catch (_) {
// Ignore environments without a console.
}
return false;
}
}
const DEFAULTS = Object.freeze({
unwrapRedirects: true,
removePing: true,
removeInlineTracking: true,
removeTrackingAttributes: true,
noReferrer: true,
stripGoogleTrackingParams: false,
stripDestinationTrackingParams: true,
unwrapAmpLinks: true,
blockTrackingBeacons: true,
removeTrackingPixels: true,
cleanShadowDom: true,
aggressiveHrefGuard: true,
forceSafeSearchOff: true,
floatingPanelEnabled: true,
debug: false
});
let settings = loadSettings();
let scanQueued = false;
let scanTimer = 0;
let lastUrl = location.href;
let observer = null;
let observerRoot = null;
let safeSearchGuard = false;
let hrefGuardInstalled = false;
let beaconGuardInstalled = false;
let imageGuardInstalled = false;
let panelHost = null;
let panelInstalled = false;
let safetyTimer = 0;
const observedShadowRoots =
typeof WeakSet === 'function' ? new WeakSet() : null;
const stats = {
linksCleaned: 0,
beaconsBlocked: 0,
pixelsBlocked: 0
};
function log(...args) {
if (settings.debug && console && typeof console.debug === 'function') {
console.debug(PREFIX, ...args);
}
}
function warn(...args) {
if (console && typeof console.warn === 'function') {
console.warn(PREFIX, ...args);
}
}
/* ------------------------------------------------------------------ */
/* Storage / compatibility */
/* ------------------------------------------------------------------ */
function getGM(name) {
try {
return typeof window[name] === 'function' ? window[name] : null;
} catch (_) {
return null;
}
}
function loadSettings() {
const gmGet = getGM('GM_getValue');
if (gmGet) {
try {
const value = gmGet(STORAGE_KEY, null);
if (value && typeof value === 'object') {
return Object.assign({}, DEFAULTS, value);
}
} catch (e) {
log('GM_getValue unavailable:', e);
}
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (raw) {
const value = JSON.parse(raw);
if (value && typeof value === 'object') {
return Object.assign({}, DEFAULTS, value);
}
}
} catch (e) {
log('localStorage unavailable:', e);
}
return Object.assign({}, DEFAULTS);
}
function saveSettings() {
const gmSet = getGM('GM_setValue');
if (gmSet) {
try {
gmSet(STORAGE_KEY, settings);
return;
} catch (e) {
log('GM_setValue unavailable:', e);
}
}
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
log('Could not save settings:', e);
}
}
function registerMenuCommand(title, callback) {
const gmRegister = getGM('GM_registerMenuCommand');
if (!gmRegister) {
return false;
}
try {
gmRegister(title, callback);
return true;
} catch (e) {
log('GM_registerMenuCommand failed:', e);
return false;
}
}
function makeToggleCommand(label, key, afterToggle) {
return registerMenuCommand(
`DTMG: ${label} ${settings[key] ? 'ON' : 'OFF'}`,
() => {
settings[key] = !settings[key];
saveSettings();
if (typeof afterToggle === 'function') {
afterToggle();
}
refreshPanelInputs();
scheduleScan(true);
}
);
}
function registerMenu() {
let anyRegistered = false;
anyRegistered = makeToggleCommand('Redirect unwrapping', 'unwrapRedirects') || anyRegistered;
anyRegistered = makeToggleCommand('Remove ping', 'removePing') || anyRegistered;
anyRegistered = makeToggleCommand('Remove inline tracking', 'removeInlineTracking') || anyRegistered;
anyRegistered = makeToggleCommand('Remove tracking attributes', 'removeTrackingAttributes') || anyRegistered;
anyRegistered = makeToggleCommand('No-referrer', 'noReferrer') || anyRegistered;
anyRegistered = makeToggleCommand('Strip Google tracking params', 'stripGoogleTrackingParams') || anyRegistered;
anyRegistered = makeToggleCommand('Strip destination tracking params', 'stripDestinationTrackingParams') || anyRegistered;
anyRegistered = makeToggleCommand('Unwrap AMP links', 'unwrapAmpLinks') || anyRegistered;
anyRegistered = makeToggleCommand('Block tracking beacons', 'blockTrackingBeacons', () => {
installBeaconGuard();
installImageBeaconGuard();
}) || anyRegistered;
anyRegistered = makeToggleCommand('Remove tracking pixels', 'removeTrackingPixels') || anyRegistered;
anyRegistered = makeToggleCommand('Clean shadow DOM', 'cleanShadowDom') || anyRegistered;
anyRegistered = makeToggleCommand('Aggressive href guard', 'aggressiveHrefGuard', () => {
installHrefGuard();
}) || anyRegistered;
anyRegistered = makeToggleCommand('Force SafeSearch OFF', 'forceSafeSearchOff', () => {
forceSafeSearchOff();
}) || anyRegistered;
anyRegistered = makeToggleCommand('Floating settings button', 'floatingPanelEnabled', () => {
if (settings.floatingPanelEnabled) {
buildPanel();
} else {
removePanel();
}
}) || anyRegistered;
anyRegistered = makeToggleCommand('Debug', 'debug') || anyRegistered;
anyRegistered = registerMenuCommand('DTMG: Clean now', () => scheduleScan(true)) || anyRegistered;
anyRegistered = registerMenuCommand(`DTMG: Version ${VERSION}`, () => {
alert(`Don't Track Me Google ${VERSION}\nCleaned: ${stats.linksCleaned} link(s)\nBlocked: ${stats.beaconsBlocked} beacon(s), ${stats.pixelsBlocked} pixel(s)`);
}) || anyRegistered;
return anyRegistered;
}
/* ------------------------------------------------------------------ */
/* In-page floating settings panel (fallback UI) */
/* ------------------------------------------------------------------ */
const PANEL_TOGGLES = [
{ key: 'unwrapRedirects', label: 'Unwrap redirect links' },
{ key: 'unwrapAmpLinks', label: 'Unwrap AMP links' },
{ key: 'noReferrer', label: 'No-referrer on links' },
{ key: 'removePing', label: 'Remove ping tracking' },
{ key: 'removeInlineTracking', label: 'Remove inline click handlers' },
{ key: 'removeTrackingAttributes', label: 'Remove tracking attributes' },
{ key: 'stripGoogleTrackingParams', label: 'Strip Google tracking params' },
{ key: 'stripDestinationTrackingParams', label: 'Strip destination tracking params' },
{ key: 'blockTrackingBeacons', label: 'Block telemetry beacons' },
{ key: 'removeTrackingPixels', label: 'Remove tracking pixels' },
{ key: 'cleanShadowDom', label: 'Clean shadow DOM' },
{ key: 'aggressiveHrefGuard', label: 'Aggressive href guard' },
{ key: 'forceSafeSearchOff', label: 'Force SafeSearch off' },
{ key: 'debug', label: 'Debug logging' }
];
let panelInputs = null;
function refreshPanelInputs() {
if (!panelInputs) {
return;
}
for (const t of PANEL_TOGGLES) {
const input = panelInputs[t.key];
if (input) {
input.checked = !!settings[t.key];
}
}
}
function buildPanel() {
if (!settings.floatingPanelEnabled || panelInstalled || !document.documentElement) {
return;
}
try {
const host = document.createElement('div');
host.id = 'dtmg-panel-host';
host.style.all = 'initial';
host.style.position = 'fixed';
host.style.zIndex = '2147483647';
host.style.bottom = '16px';
host.style.right = '16px';
const shadow = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host { all: initial; }
* { box-sizing: border-box; }
.dtmg-toggle {
width: 36px; height: 36px; border-radius: 50%;
background: #1a73e8; color: #fff; border: none;
font: bold 10px/36px Arial, sans-serif; text-align: center;
cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,.35);
}
.dtmg-panel {
display: none;
position: absolute; bottom: 44px; right: 0;
width: 260px; max-height: 360px; overflow-y: auto;
padding: 10px 12px; border-radius: 8px;
background: #ffffff; color: #202124;
box-shadow: 0 2px 12px rgba(0,0,0,.3);
font: 12px/1.4 Arial, sans-serif;
}
.dtmg-panel.open { display: block; }
.dtmg-panel h3 { margin: 0 0 6px; font-size: 13px; }
.dtmg-close {
float: right; cursor: pointer; border: none; background: none;
font-size: 13px; color: #5f6368; line-height: 1;
}
.dtmg-row { display: flex; align-items: center; gap: 6px; margin: 4px 0; }
.dtmg-row label { flex: 1; cursor: pointer; }
.dtmg-actions { margin-top: 6px; display: flex; gap: 6px; }
.dtmg-actions button {
flex: 1; padding: 4px 6px; border: 1px solid #dadce0;
background: #f1f3f4; border-radius: 4px; cursor: pointer; font-size: 11px;
}
.dtmg-footer { margin-top: 6px; font-size: 10px; color: #5f6368; }
`;
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
const toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'dtmg-toggle';
toggleBtn.title = "Don't Track Me Google settings";
toggleBtn.textContent = 'DTMG';
const panelEl = document.createElement('div');
panelEl.className = 'dtmg-panel';
const heading = document.createElement('h3');
heading.textContent = "Don't Track Me Google";
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'dtmg-close';
closeBtn.textContent = '✕';
closeBtn.addEventListener('click', () => panelEl.classList.remove('open'));
heading.appendChild(closeBtn);
panelEl.appendChild(heading);
panelInputs = {};
for (const t of PANEL_TOGGLES) {
const row = document.createElement('div');
row.className = 'dtmg-row';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `dtmg-${t.key}`;
input.checked = !!settings[t.key];
const label = document.createElement('label');
label.htmlFor = input.id;
label.textContent = t.label;
input.addEventListener('change', () => {
settings[t.key] = input.checked;
saveSettings();
if (t.key === 'forceSafeSearchOff') {
forceSafeSearchOff();
}
if (t.key === 'blockTrackingBeacons') {
installBeaconGuard();
installImageBeaconGuard();
}
if (t.key === 'aggressiveHrefGuard') {
installHrefGuard();
}
scheduleScan(true);
});
row.appendChild(input);
row.appendChild(label);
panelEl.appendChild(row);
panelInputs[t.key] = input;
}
const actions = document.createElement('div');
actions.className = 'dtmg-actions';
const cleanNowBtn = document.createElement('button');
cleanNowBtn.type = 'button';
cleanNowBtn.textContent = 'Clean now';
cleanNowBtn.addEventListener('click', () => scheduleScan(true));
const hideBtn = document.createElement('button');
hideBtn.type = 'button';
hideBtn.textContent = 'Hide button';
hideBtn.addEventListener('click', () => {
settings.floatingPanelEnabled = false;
saveSettings();
removePanel();
});
actions.appendChild(cleanNowBtn);
actions.appendChild(hideBtn);
panelEl.appendChild(actions);
const footer = document.createElement('div');
footer.className = 'dtmg-footer';
footer.textContent = `v${VERSION} · cleaned ${stats.linksCleaned}, blocked ${stats.beaconsBlocked + stats.pixelsBlocked}`;
panelEl.appendChild(footer);
toggleBtn.addEventListener('click', () => {
panelEl.classList.toggle('open');
footer.textContent = `v${VERSION} · cleaned ${stats.linksCleaned}, blocked ${stats.beaconsBlocked + stats.pixelsBlocked}`;
});
wrapper.appendChild(toggleBtn);
wrapper.appendChild(panelEl);
shadow.appendChild(style);
shadow.appendChild(wrapper);
document.documentElement.appendChild(host);
panelHost = host;
panelInstalled = true;
} catch (e) {
log('Could not build floating panel:', e);
}
}
function removePanel() {
try {
if (panelHost && panelHost.parentNode) {
panelHost.parentNode.removeChild(panelHost);
}
} catch (_) {
// Ignore removal errors.
}
panelHost = null;
panelInstalled = false;
panelInputs = null;
}
/* ------------------------------------------------------------------ */
/* URL handling */
/* ------------------------------------------------------------------ */
const REDIRECT_PATHS = new Set([
'/url',
'/imgres',
'/aclk',
'/link',
'/webhp',
'/interstitial',
'/translate',
'/ml'
]);
const TARGET_KEYS = [
'q',
'url',
'u',
'imgurl',
'imgrefurl',
'adurl',
'continue',
'destination'
];
const GOOGLE_TRACKING_PARAMS = new Set([
'gclid',
'dclid',
'gbraid',
'wbraid',
'gclsrc',
'ved',
'ei',
'sxsrf',
'iflsig',
'uule',
'gs_lcrp',
'sourceid',
'client',
'oq',
'aqs',
'hl',
'gl',
'ictx',
'sca_esv',
'sca_upv'
]);
const DESTINATION_TRACKING_PARAMS = new Set([
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'utm_id',
'utm_name',
'utm_reader',
'gclid',
'gclsrc',
'dclid',
'gbraid',
'wbraid',
'fbclid',
'igshid',
'ig_rid',
'msclkid',
'mc_eid',
'mc_cid',
'yclid',
'vero_id',
'oly_anon_id',
'oly_enc_id',
'_hsenc',
'_hsmi',
'mkt_tok',
'ref_src',
'trk',
'trkCampaign',
'si'
]);
const TRACKING_BEACON_HOSTS = new Set([
'doubleclick.net',
'google-analytics.com',
'googletagmanager.com',
'googlesyndication.com',
'googleadservices.com'
]);
const GOOGLE_BEACON_PATHS = new Set([
'/gen_204',
'/client_204',
'/csi',
'/log204',
'/generate_204'
]);
function isHttpUrl(value) {
return /^https?:\/\//i.test(value);
}
function decodeRepeatedly(value, max = 3) {
let result = value;
for (let i = 0; i < max; i++) {
let decoded;
try {
decoded = decodeURIComponent(result);
} catch (_) {
break;
}
if (decoded === result) {
break;
}
result = decoded;
}
return result;
}
function isGoogleHost(hostname) {
const host = String(hostname || '').toLowerCase().replace(/\.$/, '');
return (
host === 'google.com' ||
host.endsWith('.google.com') ||
/^google\.[a-z.]+$/.test(host) ||
/^www\.google\.[a-z.]+$/.test(host)
);
}
function isTrackingBeaconUrl(rawUrl) {
if (!rawUrl) {
return false;
}
let url;
try {
url = new URL(rawUrl, location.href);
} catch (_) {
return false;
}
const host = url.hostname.toLowerCase().replace(/\.$/, '');
if (isGoogleHost(host) && GOOGLE_BEACON_PATHS.has(url.pathname)) {
return true;
}
for (const trackingHost of TRACKING_BEACON_HOSTS) {
if (host === trackingHost || host.endsWith('.' + trackingHost)) {
return true;
}
}
return false;
}
function extractRedirectTarget(url) {
if (!url || !settings.unwrapRedirects) {
return null;
}
let parsed;
try {
parsed = new URL(url, location.href);
} catch (_) {
return null;
}
if (!isHttpUrl(parsed.href)) {
return null;
}
if (!isGoogleHost(parsed.hostname)) {
return null;
}
if (!REDIRECT_PATHS.has(parsed.pathname)) {
return null;
}
for (const key of TARGET_KEYS) {
const raw = parsed.searchParams.get(key);
if (!raw) {
continue;
}
const candidate = decodeRepeatedly(raw).trim();
if (!isHttpUrl(candidate)) {
continue;
}
try {
const target = new URL(candidate);
if (target.protocol === 'http:' || target.protocol === 'https:') {
return target.href;
}
} catch (_) {
// Ignore malformed target.
}
}
return null;
}
function unwrapAmpUrl(url) {
if (!settings.unwrapAmpLinks || !url) {
return url;
}
let parsed;
try {
parsed = new URL(url, location.href);
} catch (_) {
return url;
}
if (!isGoogleHost(parsed.hostname)) {
return url;
}
const secureMatch = parsed.pathname.match(/^\/amp\/s\/(.+)$/i);
if (secureMatch) {
try {
return new URL('https://' + secureMatch[1] + parsed.search + parsed.hash).href;
} catch (_) {
return url;
}
}
const plainMatch = parsed.pathname.match(/^\/amp\/(?!s\/)(.+)$/i);
if (plainMatch) {
try {
return new URL('http://' + plainMatch[1] + parsed.search + parsed.hash).href;
} catch (_) {
return url;
}
}
return url;
}
function stripGoogleParams(url) {
if (!settings.stripGoogleTrackingParams || !url) {
return url;
}
let parsed;
try {
parsed = new URL(url);
} catch (_) {
return url;
}
let changed = false;
for (const key of Array.from(parsed.searchParams.keys())) {
if (GOOGLE_TRACKING_PARAMS.has(key.toLowerCase())) {
parsed.searchParams.delete(key);
changed = true;
}
}
return changed ? parsed.href : url;
}
function stripDestinationTrackingParams(url) {
if (!settings.stripDestinationTrackingParams || !url) {
return url;
}
let parsed;
try {
parsed = new URL(url);
} catch (_) {
return url;
}
if (isGoogleHost(parsed.hostname)) {
// Google's own first-party params are handled separately by
// stripGoogleTrackingParams, which is opt-in.
return url;
}
let changed = false;
for (const key of Array.from(parsed.searchParams.keys())) {
if (DESTINATION_TRACKING_PARAMS.has(key)) {
parsed.searchParams.delete(key);
changed = true;
}
}
return changed ? parsed.href : url;
}
/* ------------------------------------------------------------------ */
/* SafeSearch enforcement */
/* ------------------------------------------------------------------ */
const SAFESEARCH_KEYS = new Set(['safe', 'safesearch']);
function forceSafeSearchOffInUrl(inputUrl) {
if (!settings.forceSafeSearchOff || !inputUrl) {
return inputUrl;
}
let url;
try {
url = new URL(inputUrl, location.href);
} catch (_) {
return inputUrl;
}
if (!isGoogleHost(url.hostname)) {
return inputUrl;
}
let changed = false;
for (const key of SAFESEARCH_KEYS) {
if (url.searchParams.has(key)) {
url.searchParams.set(key, 'off');
changed = true;
}
}
/*
* Only add safe=off to actual Google search surfaces.
* Do not modify the normal Google homepage unnecessarily.
*/
if (
url.searchParams.get('safe') === null &&
(url.pathname === '/search' || url.pathname === '/webhp')
) {
url.searchParams.set('safe', 'off');
changed = true;
}
return changed ? url.href : inputUrl;
}
function forceSafeSearchOff() {
if (!settings.forceSafeSearchOff || safeSearchGuard) {
return;
}
safeSearchGuard = true;
try {
const current = new URL(location.href);
if (isGoogleHost(current.hostname)) {
const cleaned = forceSafeSearchOffInUrl(current.href);
if (cleaned !== current.href) {
/*
* replaceState prevents an extra history entry.
*/
history.replaceState(history.state, '', cleaned);
lastUrl = location.href;
}
}
/*
* Update search form fields.
*/
const fields = document.querySelectorAll(
'input[name="safe"], input[name="safesearch"]'
);
for (const field of fields) {
if (field.value && field.value.toLowerCase() !== 'off') {
field.value = 'off';
}
}
/*
* Rewrite existing Google navigation links.
*/
const links = document.querySelectorAll('a[href]');
for (const a of links) {
const href = a.getAttribute('href');
if (!href) {
continue;
}
const updated = forceSafeSearchOffInUrl(href);
if (updated !== href) {
a.setAttribute('href', updated);
}
}
} catch (e) {
log('SafeSearch enforcement error:', e);
} finally {
safeSearchGuard = false;
}
}
function cleanUrl(url) {
let result = url;
const unwrapped = extractRedirectTarget(result);
if (unwrapped) {
result = unwrapped;
}
result = unwrapAmpUrl(result);
result = stripGoogleParams(result);
result = stripDestinationTrackingParams(result);
result = forceSafeSearchOffInUrl(result);
return result;
}
/* ------------------------------------------------------------------ */
/* Link cleaning */
/* ------------------------------------------------------------------ */
function removeGoogleHandlers(a) {
if (!settings.removeInlineTracking) {
return;
}
const attrs = ['onmousedown', 'onmouseup', 'onclick', 'ontouchstart', 'onpointerdown'];
for (const attr of attrs) {
if (a.hasAttribute(attr)) {
a.removeAttribute(attr);
}
}
const jsaction = a.getAttribute('jsaction');
if (jsaction) {
const cleaned = jsaction
.split(';')
.map(part => part.trim())
.filter(part => {
const lower = part.toLowerCase();
return !(
lower.startsWith('mousedown:') ||
lower.startsWith('click:') ||
lower.startsWith('touchstart:') ||
lower.startsWith('pointerdown:')
);
})
.join(';');
if (cleaned !== jsaction) {
if (cleaned) {
a.setAttribute('jsaction', cleaned);
} else {
a.removeAttribute('jsaction');
}
}
}
}
function removeTrackingAttributes(a) {
if (!settings.removeTrackingAttributes) {
return;
}
const attrs = ['data-ved', 'data-jsarwt', 'data-usg', 'data-cthref'];
for (const attr of attrs) {
if (a.hasAttribute(attr)) {
a.removeAttribute(attr);
}
}
}
function applyReferrerPolicy(a) {
if (!settings.noReferrer) {
return;
}
try {
a.referrerPolicy = 'no-referrer';
const rel = new Set(
(a.getAttribute('rel') || '')
.split(/\s+/)
.map(v => v.trim().toLowerCase())
.filter(Boolean)
);
rel.add('noreferrer');
a.setAttribute('rel', Array.from(rel).join(' '));
} catch (e) {
log('Could not apply referrer policy:', e);
}
}
function cleanLink(a) {
if (!a || a.nodeType !== 1 || a.tagName !== 'A') {
return false;
}
let changed = false;
try {
const rawHref = a.getAttribute('href');
if (!rawHref || rawHref.startsWith('#')) {
return false;
}
const marked = a.getAttribute(CLEAN_MARK) === '1';
let absolute;
try {
absolute = new URL(a.href, location.href).href;
} catch (_) {
absolute = rawHref;
}
const cleaned = cleanUrl(absolute);
if (cleaned && cleaned !== absolute && isHttpUrl(cleaned)) {
a.setAttribute('href', cleaned);
changed = true;
}
if (settings.removePing && a.hasAttribute('ping')) {
a.removeAttribute('ping');
changed = true;
}
if (settings.noReferrer) {
applyReferrerPolicy(a);
}
if (settings.removeInlineTracking) {
removeGoogleHandlers(a);
}
removeTrackingAttributes(a);
if (!marked || changed) {
a.setAttribute(CLEAN_MARK, '1');
}
if (changed) {
stats.linksCleaned++;
}
return changed;
} catch (e) {
log('Error cleaning link:', e);
return false;
}
}
function scrubTrackingPixels(root) {
if (!settings.removeTrackingPixels || !root || !root.querySelectorAll) {
return 0;
}
let count = 0;
let candidates;
try {
candidates = root.querySelectorAll(
'img[src], link[rel~="preload"][href], link[rel~="prefetch"][href]'
);
} catch (_) {
return 0;
}
for (const el of candidates) {
const isImg = el.tagName === 'IMG';
const src = isImg ? el.getAttribute('src') : el.getAttribute('href');
if (!src || !isTrackingBeaconUrl(src)) {
continue;
}
try {
if (isImg) {
el.removeAttribute('src');
el.setAttribute('data-dtmg-blocked-pixel', '1');
} else {
el.remove();
}
count++;
stats.pixelsBlocked++;
} catch (_) {
// Ignore removal errors.
}
}
return count;
}
function cleanRoot(root) {
if (!root || !root.querySelectorAll) {
return 0;
}
let count = 0;
let links;
try {
links = root.querySelectorAll('a');
} catch (_) {
return 0;
}
for (const a of links) {
if (cleanLink(a)) {
count++;
}
}
scrubTrackingPixels(root);
return count;
}
function forEachShadowRoot(root, callback, seen) {
seen = seen || new Set();
if (!root || seen.has(root) || !root.querySelectorAll) {
return;
}
seen.add(root);
let hosts;
try {
hosts = root.querySelectorAll('*');
} catch (_) {
return;
}
for (const el of hosts) {
if (el.shadowRoot) {
callback(el.shadowRoot);
forEachShadowRoot(el.shadowRoot, callback, seen);
}
}
}
function observeShadowRoot(root) {
if (
!root ||
!window.MutationObserver ||
(observedShadowRoots && observedShadowRoots.has(root))
) {
return;
}
try {
const shadowObserver = new MutationObserver(handleMutations);
shadowObserver.observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
'href',
'ping',
'onmousedown',
'onclick',
'ontouchstart',
'onpointerdown',
'jsaction'
]
});
if (observedShadowRoots) {
observedShadowRoots.add(root);
}
} catch (e) {
log('Could not observe shadow root:', e);
}
}
function cleanElement(root) {
if (!root || root.nodeType !== 1) {
return 0;
}
let count = 0;
if (root.tagName === 'A' && cleanLink(root)) {
count++;
}
if (root.querySelectorAll) {
const links = root.querySelectorAll('a');
for (const a of links) {
if (cleanLink(a)) {
count++;
}
}
scrubTrackingPixels(root);
if (settings.cleanShadowDom) {
if (root.shadowRoot) {
count += cleanRoot(root.shadowRoot);
observeShadowRoot(root.shadowRoot);
}
forEachShadowRoot(root, shadowRoot => {
count += cleanRoot(shadowRoot);
observeShadowRoot(shadowRoot);
});
}
}
return count;
}
function processAllLinks() {
if (!document.documentElement) {
return 0;
}
let count = cleanRoot(document);
if (settings.cleanShadowDom) {
forEachShadowRoot(document, shadowRoot => {
count += cleanRoot(shadowRoot);
observeShadowRoot(shadowRoot);
});
}
log(`Scan complete; changed ${count} link(s).`);
return count;
}
/* ------------------------------------------------------------------ */
/* Aggressive href guard (JS-driven late rewrites) */
/* ------------------------------------------------------------------ */
function installHrefGuard() {
if (!settings.aggressiveHrefGuard || hrefGuardInstalled) {
return;
}
try {
const proto = window.HTMLAnchorElement && window.HTMLAnchorElement.prototype;
const descriptor = proto && Object.getOwnPropertyDescriptor(proto, 'href');
if (!descriptor || !descriptor.set || !descriptor.get) {
return;
}
const originalSet = descriptor.set;
const originalGet = descriptor.get;
Object.defineProperty(proto, 'href', {
configurable: true,
enumerable: descriptor.enumerable,
get: function () {
return originalGet.call(this);
},
set: function (value) {
let outgoing = value;
try {
if (settings.aggressiveHrefGuard && typeof value === 'string') {
const absolute = new URL(value, location.href).href;
const cleaned = cleanUrl(absolute);
if (cleaned && isHttpUrl(cleaned)) {
outgoing = cleaned;
}
}
} catch (_) {
outgoing = value;
}
return originalSet.call(this, outgoing);
}
});
hrefGuardInstalled = true;
log('Installed aggressive href guard.');
} catch (e) {
log('Could not install href guard:', e);
}
}
/* ------------------------------------------------------------------ */
/* Telemetry beacon guards */
/* ------------------------------------------------------------------ */
function installBeaconGuard() {
if (!settings.blockTrackingBeacons || beaconGuardInstalled) {
return;
}
try {
if (navigator.sendBeacon) {
const originalSendBeacon = navigator.sendBeacon.bind(navigator);
navigator.sendBeacon = function (url, data) {
try {
if (settings.blockTrackingBeacons && isTrackingBeaconUrl(String(url))) {
stats.beaconsBlocked++;
log('Blocked tracking beacon:', url);
return true;
}
} catch (_) {
// Fall through to the original call.
}
return originalSendBeacon(url, data);
};
}
} catch (e) {
log('sendBeacon guard failed:', e);
}
try {
const originalFetch = window.fetch;
if (typeof originalFetch === 'function') {
window.fetch = function (input, init) {
try {
const url = typeof input === 'string' ? input : input && input.url;
if (settings.blockTrackingBeacons && url && isTrackingBeaconUrl(url)) {
stats.beaconsBlocked++;
log('Blocked tracking fetch:', url);
return Promise.resolve(new Response(null, { status: 204 }));
}
} catch (_) {
// Fall through to the original call.
}
return originalFetch.apply(this, arguments);
};
}
} catch (e) {
log('fetch guard failed:', e);
}
try {
const OriginalXHR = window.XMLHttpRequest;
if (OriginalXHR) {
const originalOpen = OriginalXHR.prototype.open;
const originalSend = OriginalXHR.prototype.send;
OriginalXHR.prototype.open = function (method, url) {
try {
this.__dtmgBlocked =
settings.blockTrackingBeacons && isTrackingBeaconUrl(String(url));
} catch (_) {
this.__dtmgBlocked = false;
}
return originalOpen.apply(this, arguments);
};
OriginalXHR.prototype.send = function () {
if (this.__dtmgBlocked) {
stats.beaconsBlocked++;
log('Blocked tracking XHR.');
return;
}
return originalSend.apply(this, arguments);
};
}
} catch (e) {
log('XHR guard failed:', e);
}
beaconGuardInstalled = true;
}
function installImageBeaconGuard() {
if (!settings.blockTrackingBeacons || imageGuardInstalled) {
return;
}
try {
const proto = window.HTMLImageElement && window.HTMLImageElement.prototype;
const descriptor = proto && Object.getOwnPropertyDescriptor(proto, 'src');
if (!descriptor || !descriptor.set) {
return;
}
const originalSet = descriptor.set;
Object.defineProperty(proto, 'src', {
configurable: true,
enumerable: descriptor.enumerable,
get: descriptor.get,
set: function (value) {
try {
if (settings.blockTrackingBeacons && isTrackingBeaconUrl(String(value))) {
stats.pixelsBlocked++;
log('Blocked tracking pixel image:', value);
return;
}
} catch (_) {
// Fall through to the original setter.
}
return originalSet.call(this, value);
}
});
imageGuardInstalled = true;
log('Installed image beacon guard.');
} catch (e) {
log('Could not install image beacon guard:', e);
}
}
/* ------------------------------------------------------------------ */
/* Event protection */
/* ------------------------------------------------------------------ */
function findAnchor(target) {
if (!target) {
return null;
}
if (target.nodeType === 3) {
target = target.parentElement;
}
if (!target || target.nodeType !== 1) {
return null;
}
if (target.closest) {
return target.closest('a[href]');
}
return null;
}
function handleLinkEvent(event) {
const a = findAnchor(event.target);
if (!a) {
return;
}
const before = a.href;
cleanLink(a);
const after = a.href;
if (before !== after) {
log('Cleaned event link:', after);
}
}
const EVENTS = ['pointerdown', 'mousedown', 'touchstart', 'click', 'auxclick', 'contextmenu'];
safeCall('registerEventListeners', () => {
for (const eventName of EVENTS) {
window.addEventListener(eventName, handleLinkEvent, {
capture: true,
passive: true
});
}
});
/* ------------------------------------------------------------------ */
/* MutationObserver + SPA handling */
/* ------------------------------------------------------------------ */
function handleMutations(mutations) {
let sawRelevantNode = false;
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
sawRelevantNode = true;
cleanElement(node);
}
}
if (mutation.type === 'attributes' && mutation.target && mutation.target.tagName === 'A') {
sawRelevantNode = true;
cleanLink(mutation.target);
}
}
if (sawRelevantNode) {
forceSafeSearchOff();
scheduleScan(false);
}
}
function startObserver() {
if (observer || !window.MutationObserver) {
return;
}
const root = document.documentElement || document;
if (!root) {
return;
}
observerRoot = root;
observer = new MutationObserver(handleMutations);
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
'href',
'ping',
'onmousedown',
'onclick',
'ontouchstart',
'onpointerdown',
'jsaction'
]
});
}
function stopObserver() {
if (observer) {
observer.disconnect();
observer = null;
observerRoot = null;
}
}
function scheduleScan(immediate) {
if (scanQueued) {
return;
}
scanQueued = true;
const run = () => {
scanQueued = false;
processAllLinks();
};
if (immediate) {
run();
return;
}
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(run, { timeout: 500 });
} else {
scanTimer = window.setTimeout(run, 50);
}
}
function watchSpaNavigation() {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
function changed() {
if (location.href === lastUrl) {
forceSafeSearchOff();
return;
}
lastUrl = location.href;
forceSafeSearchOff();
scheduleScan(true);
}
try {
history.pushState = function () {
const result = originalPushState.apply(this, arguments);
changed();
return result;
};
history.replaceState = function () {
const result = originalReplaceState.apply(this, arguments);
changed();
return result;
};
window.addEventListener('popstate', changed, true);
window.addEventListener('hashchange', changed, true);
} catch (e) {
log('SPA navigation hooks unavailable:', e);
}
}
/* ------------------------------------------------------------------ */
/* Periodic safety sweep */
/* ------------------------------------------------------------------ */
function startSafetySweep() {
if (safetyTimer) {
return;
}
/*
* Low-frequency safety sweep catches DOM changes made through APIs
* that bypass MutationObserver or aggressive Google re-rendering.
*/
safetyTimer = window.setInterval(() => {
if (document.hidden) {
return;
}
handleSafeSearchNavigation();
forceSafeSearchOff();
processAllLinks();
if (settings.floatingPanelEnabled && !panelInstalled) {
buildPanel();
}
}, 4000);
}
function handleSafeSearchNavigation() {
if (!settings.forceSafeSearchOff) {
return;
}
const current = location.href;
if (current === lastUrl) {
return;
}
lastUrl = current;
forceSafeSearchOff();
}
/* ------------------------------------------------------------------ */
/* Initialization */
/* ------------------------------------------------------------------ */
let initialized = false;
function init() {
if (initialized) {
return;
}
initialized = true;
safeCall('registerMenu', registerMenu);
safeCall('installHrefGuard', installHrefGuard);
safeCall('installBeaconGuard', installBeaconGuard);
safeCall('installImageBeaconGuard', installImageBeaconGuard);
safeCall('startObserver', startObserver);
safeCall('watchSpaNavigation', watchSpaNavigation);
safeCall('startSafetySweep', startSafetySweep);
safeCall('forceSafeSearchOff', forceSafeSearchOff);
safeCall('initialScan', () => scheduleScan(true));
if (settings.floatingPanelEnabled) {
safeCall('buildPanel', buildPanel);
}
try {
console.info(
`${PREFIX} v${VERSION} initialized on ${location.hostname}` +
(STARTUP_ERRORS.length ? ` (${STARTUP_ERRORS.length} subsystem(s) failed - see above)` : '')
);
} catch (_) {
// Ignore environments without a console.
}
log(`Initialized ${VERSION}`, settings);
}
/*
* document-start can execute before <html> exists in some managers.
* The beacon/href guards are installed immediately regardless, since
* they only touch global prototypes and don't need the DOM. Each is
* wrapped in safeCall so a failure here (e.g. a browser that exposes
* a non-configurable property) can't prevent the rest of the script,
* including basic link cleaning, from running.
*/
safeCall('installHrefGuard (early)', installHrefGuard);
safeCall('installBeaconGuard (early)', installBeaconGuard);
safeCall('installImageBeaconGuard (early)', installImageBeaconGuard);
if (document.documentElement) {
init();
} else {
const ready = () => {
document.removeEventListener('DOMContentLoaded', ready);
init();
};
document.addEventListener('DOMContentLoaded', ready, { once: true });
window.addEventListener('load', () => init(), { once: true });
}
/*
* Tiny diagnostic API for browser-console testing.
*/
try {
Object.defineProperty(window, 'DontTrackMeGoogle', {
configurable: true,
value: Object.freeze({
version: VERSION,
clean: () => processAllLinks(),
getSettings: () => Object.assign({}, settings),
getStats: () => Object.assign({}, stats),
getStartupErrors: () => STARTUP_ERRORS.slice(),
showPanel: () => {
settings.floatingPanelEnabled = true;
saveSettings();
buildPanel();
},
hidePanel: () => {
settings.floatingPanelEnabled = false;
saveSettings();
removePanel();
}
})
});
} catch (_) {
// Ignore environments that prevent window properties.
}
})();