How to check do not track
Read it in two places and compare. On the wire it is the request header DNT: 1; in the page it is navigator.doNotTrack, which returned the string "1" with the preference on and null with it off in Chrome 152. The value is a string, so === 1 never matches.
Why check this
Run this when a service claims to act on the header, when a legacy code path still branches on it, or when a privacy review asks what the application does with the preference. The check takes seven minutes and settles an argument that otherwise runs on memory.
Be clear about what the answer is worth. The specification that defined the header, Tracking Preference Expression, was published as a W3C Working Group Note on 17 January 2019 and its status section records that "there has not been sufficient deployment of these extensions (as defined) to justify further advancement, nor have there been indications of planned support among user agents, third parties, and the ecosystem at large". Browsers still send it and almost nothing acts on it.
The failure this catches is a dead branch that looks alive. A handler keyed to navigator.doNotTrack === 1 compiles, passes review, and never runs, because the value is a string. The team believes the preference is honoured and the analytics tag fires anyway.
Prerequisites
- curl 8.21 or later, and Node 22 with
puppeteer-core. - The echo server on port 9641 from How to check global privacy control, which prints the
dntheader it received. - A probe that seeds the browser preference, loads that page, then makes a second request from script. Chrome stores the choice as
enable_do_not_trackin the profile'sPreferencesfile, so a fresh profile with that key is a browser with the setting on. Save asdnt.mjs:
import { launch } from 'puppeteer-core';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const on = process.argv[2] === 'on';
const profile = mkdtempSync(join(tmpdir(), 'dnt-'));
mkdirSync(join(profile, 'Default'), { recursive: true });
writeFileSync(join(profile, 'Default', 'Preferences'), JSON.stringify({ enable_do_not_track: on }));
const browser = await launch({
executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
headless: true, userDataDir: profile,
});
const page = await browser.newPage();
await page.goto('http://127.0.0.1:9641/');
const r = await page.evaluate(async () => {
await fetch('/api/report', { method: 'POST', body: '{}' });
return {
value: JSON.stringify(navigator.doNotTrack), type: typeof navigator.doNotTrack,
win: JSON.stringify(window.doNotTrack), ms: JSON.stringify(navigator.msDoNotTrack),
one: navigator.doNotTrack === 1, str: navigator.doNotTrack === '1',
};
});
console.log(`enable_do_not_track: ${on} ${await browser.version()}`);
console.log(`navigator.doNotTrack = ${r.value} (typeof ${r.type})`);
console.log(`window.doNotTrack = ${r.win} navigator.msDoNotTrack = ${r.ms}`);
console.log(`=== 1 ? ${r.one} === "1" ? ${r.str}`);
await browser.close();
rmSync(profile, { recursive: true, force: true });
- The browser figures are one capture on one machine on 2026-09-12, with Chrome 152.0.7977.76.
Steps
- Step 1.
Send the header by hand, which is how an API or a server-side handler gets tested.
curl -s -H 'DNT: 1' http://127.0.0.1:9641/<!doctype html><title>echo</title><pre>sec-gpc: (absent) dnt: 1 user-agent: curl/8.21.0</pre>The specification requires the field value to begin with the numeric character
1for a do-not-track preference. - Step 2.
Read the default state of a fresh browser profile.
node dnt.mjs offenable_do_not_track: false Chrome/152.0.7977.76 navigator.doNotTrack = null (typeof object) window.doNotTrack = undefined navigator.msDoNotTrack = undefined === 1 ? false === "1" ? falsenull, not"0". A browser with the preference off sends no header and reports no value. - Step 3.
Turn the preference on and read the same four lines.
node dnt.mjs onenable_do_not_track: true Chrome/152.0.7977.76 navigator.doNotTrack = "1" (typeof string) window.doNotTrack = undefined navigator.msDoNotTrack = undefined === 1 ? false === "1" ? trueThe last line is the whole trap on one row: the strict comparison with the number fails, the one with the string passes.
- Step 4.
Read the echo server's terminal for the requests that run produced.
… GET / sec-gpc: (absent) dnt: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36 POST /api/report sec-gpc: (absent) dnt: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36 GET /favicon.ico sec-gpc: (absent) dnt: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36The document, the scripted POST and the icon all carry the header. Server code can read it on any route, including the analytics endpoint.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| dnt: 1 on the request | The visitor has the preference on | Decide, and document, what your service does about it. |
| navigator.doNotTrack is null | The preference is off in this profile | Turn it on before testing the branch, or the branch is never covered. |
| navigator.doNotTrack is "1" | The preference is on | Compare against the string, never the number. |
| Header present, no behaviour change | The branch is dead or absent | If the privacy policy promises otherwise, that is a defect worth a ticket. |
Common mistakes
What to check next
- How to check global privacy control: the successor signal, and the one with a legal hook.
- How to check if a site honors global privacy control: how to prove a signal changes behaviour rather than reaching the server.
- How to check browser fingerprinting: the preference is one more bit a page can read about the visitor.
- How to check cookies on a website: what is stored while the header is being ignored.
FAQ
How do I check do not track in JavaScript?
Read navigator.doNotTrack and compare it with the string "1". With the preference off Chrome 152 returned null, so treat null and undefined as "no preference expressed" rather than as a refusal.
What does a do not track request look like?
A request header, DNT: 1, on every request the browser makes: the document, subresources, and calls from script. Step 4 shows all three carrying it from one page load.
Is do not track still supported?
Browsers still expose the preference and still send the header. The standard behind it is a discontinued W3C Note, and almost no recipient acts on it. Test what your own service does with it, and do not assume anyone else does anything.
Should we honour DNT or GPC?
GPC is the signal with enforcement behind it. Honour DNT only if you decide to, and then test it the same way: send the header, then measure what changed in cookies, tags and stored records.
Verified
Verified by Maks Vernycurl 8.21.0Node 22.23.2Chrome 152.0.7977.76
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic7 minpublished updated Maks Verny