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

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

Steps

  1. 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 1 for a do-not-track preference.

  2. Step 2.

    Read the default state of a fresh browser profile.

    node dnt.mjs off
    
    enable_do_not_track: false   Chrome/152.0.7977.76
    navigator.doNotTrack = null (typeof object)
    window.doNotTrack = undefined   navigator.msDoNotTrack = undefined
    === 1 ? false    === "1" ? false

    null, not "0". A browser with the preference off sends no header and reports no value.

  3. Step 3.

    Turn the preference on and read the same four lines.

    node dnt.mjs on
    
    enable_do_not_track: true   Chrome/152.0.7977.76
    navigator.doNotTrack = "1" (typeof string)
    window.doNotTrack = undefined   navigator.msDoNotTrack = undefined
    === 1 ? false    === "1" ? true

    The last line is the whole trap on one row: the strict comparison with the number fails, the one with the string passes.

  4. 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.36

    The 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

Sign: A DNT branch never runs, and the code looks correct.Cause: navigator.doNotTrack is a string. The capture above shows === 1 returning false and === '1' returning true in the same page load. Any comparison with the number, or with a boolean, silently fails.
Sign: An older snippet reads window.doNotTrack or navigator.msDoNotTrack and always reports no preference.Cause: Both were undefined in Chrome 152 with the preference on. A fallback chain that checks those first still ends at navigator.doNotTrack, but a snippet that reads only the legacy names reports nothing on every modern browser.
Sign: The test profile always shows the preference off.Cause: The setting lives in the browser profile, and every fresh profile starts with it off. Automation that launches a clean profile per run tests the off state forever. Seed enable_do_not_track in the profile, as dnt.mjs does.
Sign: DNT is treated as recorded consent.Cause: It is a request header anyone can send or strip, and the specification that defined it stopped for lack of adoption. A consent record needs a stored decision with a timestamp and a scope, not a header on one request.

What to check next

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.

basic7 minpublished updated Maks Verny