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
- Chrome. This page used version 152.0.7977.76 on Windows. Every figure below is one capture on one machine on 2026-09-11, and a site of this kind loads a different set on the next visit.
- Node 22 or later and
puppeteer-core(npm i puppeteer-core). - curl, for the markup comparison and for reading the first bytes of a script.
- A registrable domain is the name one organisation registers, such as
cloudflare.comorco.ukplus one label. It is the level at which cookies and CSP source lists work, which is why the grouping below uses it.
Steps
- Step 1.
Group every script response by registrable domain. Save this as
third-party.mjsand runnode 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>g_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.jsOf 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.
- 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.
- 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 200var 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.
- Step 4.
Try to confirm that ownership from DNS, and watch it fail.
nslookup -type=cname ot.www.cloudflare.comServer: 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
What to check next
- How to check if a javascript file is loaded: the same list read per file, including the ones that arrive and never run.
- How to check console errors on a website: third-party code is the usual source of the errors nobody owns.
- How to check CSP header: the source list that decides which of these domains a browser will fetch from.
- How to check subresource integrity: what to add once the inventory is agreed and the URLs are fixed.
- How to check number of requests on a page: the whole request list, of which scripts were 93 here.
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.
Related on this site
intermediate9 minpublished updated Maks Verny