Tracking inside an embedded widget
A booking widget, a payment step or a chat box in an iframe has its own dataLayer. Why Preview shows one event where the log shows two, and what actually crosses the boundary.
An iframe is a separate document. It has its own window, its own
dataLayer, possibly its own GTM container, and no obligation to tell the parent page anything.
Most tracking confusion around booking widgets, payment steps and chat boxes comes from treating that
boundary as if it were not there.
What GTM Preview does at the boundary
Preview attaches to the frame you opened it on. Events in another frame are either invisible or appear
once when they happened twice. That is not a bug in Preview; it is the scope it was given. But it does mean
"Preview showed one page_view" is not evidence that only one was sent.
Same-origin and cross-origin are different problems
- Same-origin — the iframe is on your own domain. You can reach into it, read its dataLayer, and push into it. Everything is possible and the only real question is whether anyone remembered to.
- Cross-origin — the widget is on the vendor's domain. You cannot touch its DOM, its
dataLayer or its cookies. The only channel is
postMessage, and it exists only if the vendor built it. If they did not, no amount of GTM configuration will get an event out of that frame.
The three failures worth knowing
- The conversion happens in someone else's property. A booking widget on the vendor's domain fires the vendor's own GA4. The event exists — just not in your account. What you get is a session that stops at the widget and no purchase, which reads like a tracking bug and is actually an architecture one.
- The payment step is a cross-domain hop wearing a costume. Whether it is an iframe or a redirect, leaving your origin without the linker starts a new client ID. The visitor comes back looking like a new person referred by the payment provider.
- The same container in both frames. Install GTM in the parent and again inside a
same-origin iframe and you get two
page_viewevents for one page. Sessions and engagement inflate quietly, and Preview — watching one frame — shows you a single, entirely healthy event.
Tag Master listens in every frame, not just the top one. Each dataLayer push is labelled with the frame it came from and the iframe's hostname, and a "Main frame" filter lets you hide or show them — which is how the duplicate case becomes obvious instead of invisible.
Checking it
- Load the page with the widget and count
page_viewevents with the frame filter off. More than one means the container is installed twice. - Interact with the widget. If nothing appears in any frame, the widget is cross-origin and silent, and the answer is a conversation with the vendor rather than a trigger.
- If events do appear from the iframe, check which account they carry —
tidfor GA4, the pixel ID for Meta. A vendor's own ID means the data is going to the vendor. - Across the widget boundary, check
cidthe way you would for any domain hop.
If the vendor gives you postMessage
Then the pattern is: the widget posts an event, a listener in the parent receives it and pushes to the
parent's dataLayer, and your own tags fire from there. Two things to insist on. Check
event.origin against the expected host — a listener that accepts messages from anywhere will
happily accept a purchase event from a page you did not write. And make sure the payload carries an order
identifier, so the event can be deduplicated against whatever the vendor also sends.
Related guides
- How to debug server-side GTM
- Debugging Consent Mode V2: gcs, gcd and the violations nobody catches
- Tag Assistant alternative in 2026
Try it on your own site
Tag Master is free, needs no account, and collects no data.
Add to Chrome — Free