How to check which third party scripts a page loads

Load the page in a driven Chrome, keep every response whose resource type is script, and group the URLs by registrable domain. On www.cloudflare.com that returned 93 script responses across 7 domains: 83 on cloudflare.com and 10 on six domains the site's own web team does not write.

Why check this

Third-party code executes with the same privileges as the site's own. It reads the DOM, reads first-party cookies, and can add more scripts after it runs. The inventory belongs in the release checklist for anything that touches marketing tags, and in the review that follows a consent or privacy change.

The failure it catches is a tag that arrived without a decision. A campaign adds one container to the tag manager, the container loads four vendors, and none of the four appears in the repository, in the CSP, or in the privacy notice. The page still works, so nothing raises an alarm.

Prerequisites

Steps

  1. Step 1.

    Group every script response by registrable domain. Save this as third-party.mjs and run node third-party.mjs https://www.cloudflare.com/.

    // third-party.mjs   run: node third-party.mjs <url>
    import { launch } from 'puppeteer-core';
    const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
    // Registrable domain: the level a cookie and a CSP source list work at.
    const TWO_LEVEL = ['co.uk', 'com.au', 'co.jp', 'org.uk', 'com.br'];
    const registrable = (u) => {
      const p = new URL(u).hostname.split('.');
      return TWO_LEVEL.includes(p.slice(-2).join('.')) ? p.slice(-3).join('.') : p.slice(-2).join('.');
    };
    const browser = await launch({ executablePath: CHROME, headless: true });
    const page = await browser.newPage();
    const scripts = [];
    page.on('response', (r) => {
      if (r.request().resourceType() === 'script') scripts.push({ url: r.url(), status: r.status() });
    });
    await page.goto(process.argv[2], { waitUntil: 'networkidle2' });
    await new Promise((r) => setTimeout(r, 2000));
    const own = registrable(process.argv[2]);
    const byDomain = {};
    for (const s of scripts) (byDomain[registrable(s.url)] ??= []).push(s);
    console.log(`${scripts.length} script responses, ${Object.keys(byDomain).length} registrable domains`);
    for (const [d, list] of Object.entries(byDomain).sort((a, b) => b[1].length - a[1].length)) {
      const hosts = [...new Set(list.map((s) => new URL(s.url).hostname))].join(', ');
      console.log(`${String(list.length).padStart(3)}  ${d.padEnd(22)}${d === own ? 'page domain  ' : 'other domain '}${hosts}`);
    }
    console.log(`\nscripts served from a domain other than ${own}:`);
    for (const s of scripts.filter((x) => registrable(x.url) !== own)) console.log(`  ${s.status} ${s.url}`);
    console.log(`\nscripts on ${own} but not on the page's own hostname:`);
    const host = new URL(process.argv[2]).hostname;
    for (const s of scripts.filter((x) => registrable(x.url) === own && new URL(x.url).hostname !== host)) {
      console.log(`  ${s.status} ${s.url}`);
    }
    await browser.close();
    
    93 script responses, 7 registrable domains
    83  cloudflare.com        page domain  www.cloudflare.com, ot.www.cloudflare.com
    4  adsrvr.org            other domain js.adsrvr.org
    2  marketo.net           other domain munchkin.marketo.net
    1  cloudflareinsights.comother domain static.cloudflareinsights.com
    1  googletagmanager.com  other domain www.googletagmanager.com
    1  demandbase.com        other domain tag.demandbase.com
    1  ads-twitter.com       other domain static.ads-twitter.com
    
    scripts served from a domain other than cloudflare.com:
    200 https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495
    200 https://www.googletagmanager.com/gtm.js?id=GTM-NDGPDFZ&gtg_health=1
    200 https://js.adsrvr.org/up_loader.3.0.0.js
    200 https://js.adsrvr.org/up_loader.1.1.0.js
    200 https://tag.demandbase.com/1be41a80498a5b73.min.js
    200 https://static.ads-twitter.com/uwt.js
    200 https://munchkin.marketo.net/munchkin-beta.js
    200 https://munchkin.marketo.net/165/munchkin.js
    200 https://js.adsrvr.org/universal_pixel.js
    304 https://js.adsrvr.org/universal_pixel.js
    
    scripts on cloudflare.com but not on the page's own hostname:
    200 https://ot.www.cloudflare.com/ot/scripttemplates/otSDKStub.js
    200 https://ot.www.cloudflare.com/ot/scripttemplates/202503.1.0/otBannerSdk.js

    Of the ten responses from other domains, one is Cloudflare's own analytics beacon on a second brand domain. The rest belong to a tag manager, an advertising platform, a marketing automation vendor, an intent-data vendor and an ad network.

  2. Step 2.

    Ask how many of those the HTML declared.

    curl -s --compressed https://www.cloudflare.com/ | grep -o -E '<script[^>]+src="[^"]+"'
    
    <script async type="text/javascript" src="https://ot.www.cloudflare.com/ot/scripttemplates/otSDKStub.js"
    <script type="module" src="/_astro/_layout.astro_astro_type_script_index_0_lang.MkjUBwT7.js"
    <script type="module" src="/_astro/_ai-search-modal.astro_astro_type_script_index_0_lang.B0TZFBn6.js"

    Three tags. Not one of the six other domains from step 1 appears in the markup, so a grep of the HTML reports zero third-party scripts on a page that loads ten of them. Each arrived because another script asked for it.

  3. Step 3.

    Name the owner of the one first-party host that is not the site's own hostname. Read what the file starts with.

    curl -s https://ot.www.cloudflare.com/ot/scripttemplates/otSDKStub.js | head -c 200
    
    var OneTrustStub=(t=>{var e,a,o,r,i,l=new function(){this.optanonCookieName="OptanonConsent",this.optanonHtmlGroupData=[],this.optanonHostData=[],this.genVendorsData=[],this.vendorsServiceData=[],this
    …

    The consent SDK is OneTrust's code, served from a hostname inside the site's own registrable domain. Grouping by domain filed it as first party. It is vendor code, and the cookie it names is set on the site's own domain, where cookie controls aimed at third parties never reach it.

  4. Step 4.

    Try to confirm that ownership from DNS, and watch it fail.

    nslookup -type=cname ot.www.cloudflare.com
    
    Server:  dns.google
    Address:  8.8.8.8
    
    www.cloudflare.com
    primary name server = jule.ns.cloudflare.com
    responsible mail addr = dns.cloudflare.com
    serial  = 2413910121
    …

    No CNAME, only the parent zone's authority record. A proxied vendor hostname gives up nothing to DNS. The response body in step 3 is the evidence, and the answer to "who controls this" comes from the bytes, not from the name.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A domain the organisation does not own | Third-party code, plainly | Find who added it and when. Add it to the CSP source list or remove it. | | A subdomain of the page's own domain, unfamiliar path | A vendor on a first-party hostname | Read the first bytes of the file. Domain grouping cannot answer this. | | One domain with several scripts | A loader pulled its own dependencies | Treat the loader as the decision point. Blocking the child leaves the parent retrying. | | The same URL twice, 200 then 304 | A revalidated response, one file | Deduplicate by URL before quoting a count. | | A third-party domain with no script rows | A pixel or a beacon, not executing code | Different risk. Keep the two inventories separate. |

Common mistakes

Sign: A domain grouping reports the site as clean, and a vendor is running on the page.Cause: ot.www.cloudflare.com sits inside the page's own registrable domain, so it grouped with the 83 first-party files. Its first 200 bytes begin with var OneTrustStub, and a CNAME lookup returns nothing because the host is proxied. Read the bytes of every first-party hostname the team does not recognise.
Sign: A grep of the HTML finds no third-party scripts.Cause: The markup here declared three script tags and the browser loaded 93 files from seven domains. Every one of the six other domains was pulled at runtime by a tag manager or by a vendor loader. A markup audit measures intent, not what runs.
Sign: The count of third-party domains changes depending on who is asked.Cause: This one load touched 19 registrable domains across all resource types and only seven of them served a script. Pixels, beacons and XHR targets are separate inventories with separate risk. State which one a number refers to.
Sign: Ten third-party script responses are reported as ten third-party scripts.Cause: One of the ten was js.adsrvr.org/universal_pixel.js appearing twice, once 200 and once 304. Responses are not files. Deduplicate by URL, and keep the status alongside so a revalidation is visible.

What to check next

FAQ

How do I identify third party scripts on a website?

Record every response with a script resource type during one load, then group the URLs by registrable domain. Anything outside the domain the organisation registered is third party. Check first-party subdomains by reading the file's opening bytes.

How do I find external scripts on a page without a driver?

Open DevTools, Network tab, filter to JS, add the Domain column from the column header context menu, and sort by it. That gives the same grouping by hand, one page at a time.

Is a script on the site's own domain always first party?

No. Vendors ship a hostname inside the customer's domain so their cookies count as first party. The consent SDK above is served from the site's domain and is OneTrust code. The file's contents settle it.

Why does the network list show more third-party domains than scripts?

Most third parties never send executable code. They receive a pixel, a beacon or an XHR. In this capture 19 domains were contacted and 7 served a script.

Verified

Verified by Maks VernyChrome 152.0.7977.76curl 8.21.0node 22.23.2puppeteer-core 25.10.0

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.

intermediate9 minpublished updated Maks Verny