How to detect user-defined window globals with JavaScript (Enhanced)
An async, error-resilient globals detector: spins up an iframe baseline, categorises the diff by type, and lets you exclude known third-party noise.
The original window-globals snippet from 2023 was a tiny IIFE that compared window against a fresh iframe and dumped everything it found. Fine for a one-off audit. Not fine for the cases I actually run into now: third-party scripts that throw on property access, sites where the iframe loads slowly, analytics noise drowning the result, or “what’s in here?” queries I want to time across a thousand experiments.
So the same pattern, async and defensive:
const detectWindowGlobals = async (options = {}) => {
const { excludePatterns = [], performanceMode = true } = options;
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;';
if (!document.body) throw new Error('Document body not available');
document.body.appendChild(iframe);
await new Promise(resolve => {
if (iframe.contentWindow) resolve();
else iframe.onload = resolve;
});
const cleanWindow = iframe.contentWindow;
if (!cleanWindow) throw new Error('Failed to access iframe window');
const currentProps = Object.getOwnPropertyNames(window);
const baselineProps = Object.getOwnPropertyNames(cleanWindow);
const customProps = currentProps.filter(prop =>
!baselineProps.includes(prop) &&
!excludePatterns.some(pattern => new RegExp(pattern).test(prop))
);
const globals = { functions: {}, objects: {}, arrays: {}, primitives: {}, undefined: {}, symbols: {} };
const errors = [];
customProps.forEach(prop => {
try {
const value = window[prop];
const type = typeof value;
if (value === null) globals.primitives[prop] = null;
else if (value === undefined) globals.undefined[prop] = undefined;
else if (type === 'symbol') globals.symbols[prop] = value.toString();
else if (type === 'function') globals.functions[prop] = { name: value.name || 'anonymous', length: value.length };
else if (Array.isArray(value)) globals.arrays[prop] = { length: value.length };
else if (type === 'object') globals.objects[prop] = { constructor: value.constructor?.name || 'Unknown', keys: Object.keys(value).length };
else globals.primitives[prop] = value;
} catch (error) {
errors.push({ property: prop, error: error.message });
}
});
iframe.remove();
return { globals, errors, summary: { customProps: customProps.length, errors: errors.length } };
};
The production version in the Toolbox also tracks timing and has a verbose performanceMode: false path that returns object previews — trimmed here for legibility.
What changed from the 2023 version
Async iframe handling. The 2023 IIFE assumed iframe.contentWindow was available the moment the iframe was appended. That’s true on most pages, false on heavy ones with slow about:blank initialisation. The async version awaits a real signal — either contentWindow is already there or the iframe’s onload fires.
Error boundaries. Some third-party scripts attach properties whose getters throw. The original happily crashed. The async version wraps each window[prop] access in try/catch and pushes the failure into a results.errors array — you find out which property choked instead of losing the whole audit.
Pattern exclusion. Audits across our six clients turned up the same analytics noise every time (gtag, ga, dataLayer, _gaq). Pass them as excludePatterns: ['^gtag', '^ga', '^dataLayer', '^_gaq'] and the output focuses on what’s actually unfamiliar.
Where it earns its keep
// Default: every custom global, all categories
const analysis = await detectWindowGlobals();
// Quiet the analytics noise
const filtered = await detectWindowGlobals({
excludePatterns: ['^gtag', '^ga', '^dataLayer', '^_gaq']
});
// Verbose for "what's actually inside that object"
const detailed = await detectWindowGlobals({ performanceMode: false });
In practice the filtered call is what I run before shipping a new experiment to a client site — anything new in customProps that isn’t ours is something to investigate. The detailed call is what I run when something is ours but I can’t remember what shape it takes.
Two years on from the original, the snippet does the same job — find what’s been bolted onto window — but now it survives the kinds of pages it was always supposed to survive.
Happy debugging 🔍