Why this happens
Tealium's utag.js loads asynchronously by design so it doesn't block page render - anything that calls utag.* before that script parses and executes finds no such global yet.
- Common triggers: an inline
onclickhandler that callsutag.link()directly, a script tag placed above the Tealium loader snippet, a framework route change firing before hydration completes, or a tag inside Tealium itself trying to call back intoutagbefore iQ's own bootstrap finishes. - Private/incognito-specific cause: Safari ITP, Firefox Enhanced Tracking Protection, and browser-bundled ad-block lists are frequently stricter in private windows, and tag.js served from a CDN can get pattern-matched as a "tracker" and blocked outright. That isn't a timing race - the file never arrives at all.
Fix it
- Confirm the Tealium loader snippet sits in
<head>and is the standard async loader, not a hand-edited synchronous version with a straydefer/asyncattribute removed. - Never call
utag.*directly from an inline HTML attribute. Route the call through a wrapper that checkstypeof utag !== "undefined" && typeof utag.link === "function"before calling, and no-ops otherwise. - For calls that must fire early (first-interaction tracking), queue them instead: push an object describing the event to a plain array (
window.utagq = window.utagq || []; window.utagq.push({...})) and drain that queue from a Tealium extension or a loader callback once the library is confirmed ready - don't callutag.linkdirectly from application code at that point. - For SPA route-change tracking, hook the call to the same lifecycle event your app already uses for routing, not a fixed
setTimeout- and still guard it with thetypeofcheck above, since a route change can complete before utag.js has loaded. - For the private-mode-only case specifically, reproduce in an actual private/incognito window (not just "disable extensions"), open the Network tab, and check whether the tag.js request itself appears and succeeds. If it's blocked or missing, this isn't a code bug - the fix is moving the library behind a first-party path (server-side or first-party proxying) or accepting that segment isn't measurable client-side.
How to verify it worked
- Open DevTools Console and run
typeof utagat every point in the page lifecycle where your code calls it - it should read"object", never"undefined". - Open the Network tab, filter by "utag", and confirm the tag.js (and utag.sync.js if used) requests return 200, not blocked or cancelled - a red "blocked" entry there is the private-mode signature.
- Reproduce in Tealium's own Live/Trace mode alongside a real private-browsing session side by side: a standard window should show the event in trace; a private window either shows it too (fixed) or shows the request never leaving the browser (confirms the tracking-protection cause, not a code cause).
- Add a one-line
console.warninside your guarded wrapper's "not ready" branch during testing, so a still-broken setup shows visible evidence in the console instead of a silent no-op that looks identical to success.
One check before you assume it's a bug: confirm the error is reproducible on the live page over a real network request, not just a local/staging build with a different (or missing) loader snippet - a surprising share of "utag is not defined" reports trace back to a staging environment that never had the snippet added at all.