iframe İçinde Oluşan Event Parent’a Ulaşmıyor
Saha tablosu tanıdık: Cookiebot’tan farklı bir CMP iframe içinde çalışıyor, kullanıcı consent verdi ama GA4 hâlâ gcs=G100 ile event yolluyor. Daha sinsi varyantı GA4 veya GTM’in Shopify Custom Pixel olarak yüklendiği ve pixel ayarlarında Customer privacy → Permission: Not required seçildiği durum. Bu ayar pixel’in koşulsuz çalışmasına izin veriyor (Shopify dokümanı: “The pixel will always run”); consent kontrolü pixel kodu içinde elle yazılmadığı sürece GA4 Enhanced Measurement otomatik event’leri (page_view, scroll, click, form_start, form_submit, file_download, video_start) sandbox’tan veri yolluyor. Storefront tarafında kullanıcı reddetmiş olabilir; sandbox bunu bilmiyor çünkü consent state iframe sınırından geçirilmemiş. Sonuç: storefront consent tercihi ile pixel’den giden event’lerin consent argümanı (veya argümanın yokluğu) çelişiyor. İyzico 3DS iframe’inden ödeme döndü, purchase event’i fire olmadı. Shopify Custom Pixel ile yazdığın script storefront’taki gerçek URL’i göremiyor. Calendly embed’inde randevu alındı, GA4 dataLayer’da event yok. A/B test framework’ünün iframe variant’ı tetiklendi, dataLayer’a düşmedi.
Çoğunun teknik kökü aynı: tarayıcı same-origin policy, farklı origin’deki window’lar arasında doğrudan DOM erişimini engelliyor. Origin tanımı scheme + host + port üçlüsü; biri bile farklıysa iki window birbirinin DOM’unu okuyamıyor1. Çözüm omurgası window.postMessage; cross-origin sınırını kontrollü biçimde aşmak için tasarlanmış API2.
Shopify Custom Pixel istisnası ayrı bir kategori. Sandbox bilinçli olarak window.parent’a kapalı, postMessage akışı buradan çözüm üretmiyor (§7 ayrıntıda). Çözüm pixel-içi kalır: pixel kodu içinde consent argümanlarının elle ele alınması, Customer privacy → Permission: Required seçilmesi veya CMP’nin Shopify pixel’e kendi event’ini emit etmesi (popüler CMP’lerin çoğu bunu yapıyor). Bu yazının geri kalanı postMessage’in iş gördüğü senaryolar üzerine kurulu; Shopify pixel consent akışı ayrı bir yazının konusu.
Bu yazı postMessage’i tracking pratiğinde nasıl güvenli kuracağını anlatıyor. Origin kavramının URL üzerindeki yansıması tracking için URL anatomisi yazısında. Bu yazı origin doğrulamayı somut iframe senaryolarında derinleştirir.
postMessage Temeli
API üç parça: sender, receiver, mesaj.
// Sender (iframe içinden parent'a)
window.parent.postMessage(
{
type: "tracking.consent.update",
version: "1.0",
eventId: crypto.randomUUID(),
payload: { ad_storage: "granted", analytics_storage: "granted" },
},
"https://shop.example.com",
);
// Receiver (parent)
const ALLOWED_ORIGIN = "https://cmp.example.com";
const seenEvents = new Set();
window.addEventListener("message", (event) => {
if (event.origin !== ALLOWED_ORIGIN) return;
if (event.source !== document.getElementById("cmp-frame").contentWindow)
return;
if (!event.data || typeof event.data !== "object") return;
if (event.data.type !== "tracking.consent.update") return;
if (seenEvents.has(event.data.eventId)) return;
seenEvents.add(event.data.eventId);
handleConsent(event.data.payload);
});
Üç doğrulama katmanı:
event.originexact match. Hangi origin’den geldiğini kontrol et.endsWithveyaincludeskullanma. Subdomain wildcard ihtiyacı varsa allow-list +new URL(event.origin).hostnamepattern’i.event.sourcereferans kontrolü. Aynı origin’den başka bir iframe de mesaj gönderebilir; multi-iframe sayfada spoof riski. Beklediğin frame’incontentWindow’una karşı eşitlik kontrolü yap.- Payload schema validation. Mesaj objesi serialize edilebilir olmak zorunda (
structured clone algorithm)3. Function, DOM node, WeakMap gönderilemez. Object, Array, Map, Set, ArrayBuffer, Blob, File gönderilebilir; circular references desteklenir.
targetOrigin parametresinde '*' kullanmak mutlak yasak değil ama hassas veri varsa risklidir: mesaj başka origin’lere de iletilebilir. Public, non-sensitive event’lerde (örn. widget ready sinyali) kabul edilebilir; her durumda receiver tarafı güçlü validation yapmalı2.
Origin Doğrulama Tuzakları
Origin kavramı tracking için URL anatomisi yazısında tanıtıldı; pratikte postMessage ile karşılaşılan tuzaklar:
endsWith/includes saldırısı. event.origin.endsWith(".example.com") yazarsan evil-example.com bu kontrolü geçer. Doğru pattern:
const ALLOWED_HOSTS = new Set(["shop.example.com", "checkout.example.com"]);
const url = new URL(event.origin);
if (url.protocol !== "https:") return;
if (!ALLOWED_HOSTS.has(url.hostname)) return;
window.origin overwrite riski. Modern tarayıcılarda window.origin global’i Object.defineProperty ile prototype kirliliği üzerinden manipüle edilebilir. window.location.origin veya hard-coded constant tercih edilmeli.
IDN/Punycode. xn--exmple-cua.com ile examplé.com aynı domain’in iki gösterimi. Tarayıcı event.origin’i Punycode (xn--) formunda verir; allow-list’i Punycode formunda tut, human-readable formla karşılaştırma yapma.
null origin. Şu senaryolarda gelir: sandbox iframe (allow-same-origin flag’i yok), file:// URL, data: URI, bazı browser extension context’leri. Tracking akışında null origin reject edilmeli; legitimate kaynak değil.
Schema Validation ve Replay Protection
Receiver’ın aynı origin’den birden çok script ile karşılaşma ihtimali var (CMP iframe + analytics script + üçüncü taraf widget). Type namespace ayrımı zorunlu:
const HANDLERS = {
"tracking.consent.update": handleConsent,
"tracking.payment.complete": handlePurchase,
"tracking.form.submit": handleFormSubmit,
};
const handler = HANDLERS[event.data.type];
if (!handler) return;
Replay protection. Aynı payment.complete event’i network gecikmesi veya kullanıcının back/forward navigation’ı ile iki kez gelebilir. eventId ile dedup buffer kritik; özellikle e-commerce’de duplicate purchase doğrudan attribution ve gelir raporunu bozar.
Sender authentication. Çoğu tracking senaryosunda type + payload schema yeterli. Payment veya fintech akışında parent ile iframe arasında shared secret üzerinden HMAC veya JWT-style signature ekle. Widget veya genel analytics event’lerinde signature abartı, validation maliyeti veriden yüksek.
Envelope convention:
{
type: "tracking.payment.complete",
version: "1.2",
eventId: "uuid-v4",
timestamp: 1747300000000,
nonce: "random-string",
payload: { /* event-specific */ }
}
Consent Banner iframe → Tracking Script
İlk düzeltilmesi gereken yaygın yanlış varsayım: “CMP’ler iframe içinde çalışır” doğru değil. Pratikte üç farklı pattern var.
Script-level CMP’ler (Cookiebot, Iubenda). Parent sayfayla aynı window’da çalışır, doğrudan gtag('consent', 'update', ...) ve dataLayer.push yapar. postMessage gerektirmiyor; consent state aynı JS context içinde okunup yazılır4.
Hybrid (OneTrust). Standart deployment script-level. AMP, in-app webview veya bazı sandbox senaryolarında iframe + postMessage akışı devreye girer: { sentinel: 'amp', type: 'send-consent-data', payload: {...} } formatı.
Custom embedded CMP. Kendi geliştirdiğin veya iframe içinde barındırılan üçüncü taraf consent UI’ı; postMessage zorunlu.
Custom akış için race condition kritik. Iframe içindeki CMP consent.update mesajını gönderdiğinde parent’taki Consent Mode v2 listener’ı henüz yüklenmemiş olabilir; tracking script’leri default denied state ile fire olur, kullanıcının verdiği consent kaybolur.
Handshake pattern:
// Parent: ready sinyalini hemen gönder
const cmpFrame = document.getElementById("cmp-frame");
cmpFrame.addEventListener("load", () => {
cmpFrame.contentWindow.postMessage(
{ type: "tracking.parent.ready" },
"https://cmp.example.com",
);
});
// Iframe: parent ready olmadan mesaj kuyruğa
const messageQueue = [];
let parentReady = false;
window.addEventListener("message", (event) => {
if (event.origin !== PARENT_ORIGIN) return;
if (event.data.type === "tracking.parent.ready") {
parentReady = true;
messageQueue.forEach((msg) =>
window.parent.postMessage(msg, PARENT_ORIGIN),
);
messageQueue.length = 0;
}
});
function sendConsent(payload) {
const msg = {
type: "tracking.consent.update",
payload,
eventId: crypto.randomUUID(),
};
if (parentReady) {
window.parent.postMessage(msg, PARENT_ORIGIN);
} else {
messageQueue.push(msg);
}
}
Google Consent Mode v2 default state denied olmalı, update sonrası kuyruğa alınmış event’ler flush edilir5.
Payment iframe Completion Signal
Eski blog yazılarında “Stripe iframe’inden postMessage ile payment success al” pattern’i sık görülür. 2026 itibarıyla bu doğru değil.
Stripe Elements. stripe.confirmPayment() Promise döner; 3DS sonrası return_url’e redirect yapılır, payment_intent query parametresinden status okunur. Stripe iframe’inden parent’a postMessage Stripe’ın internal detail’ı; SDK üzerinden geliştiriciye expose edilmiyor6.
Paddle Overlay. Paddle.Initialize({ eventCallback: (data) => { ... } }) ile event’ler alınır (checkout.completed, checkout.payment.initiated). Raw postMessage değil, SDK abstraction7.
İyzico Checkout Form. callbackUrl + server-side webhook + getAuthResponse API çağrısı pattern’i. 3DS iframe çıkışında token callback URL’e POST edilir; client-side postMessage akışı resmi dokümanda yok.
Custom 3DS iframe (PSP entegrasyonu). Kendi acquirer veya bank 3DS challenge’ını iframe içinde gösteriyorsan postMessage zorunlu hale gelir. Acquired.com gibi PSP’lerin pattern’i:
// 3DS iframe içinden parent'a
window.parent.postMessage(
{
type: "payment.3ds.complete",
eventId: crypto.randomUUID(),
status: "authenticated",
transactionId: tx.id,
},
PARENT_ORIGIN,
);
3DS akışı vendor’a göre üç biçim alır: top-level redirect, popup window, iframe challenge. Iframe→parent messaging her vendor’da garanti değil; entegrasyon dokümanını oku, varsayma.
Conversion tracking’de purchase event’in eventId’si server-side gateway’e de gönderilmeli; GA4 ve Meta CAPI deduplication aynı ID üzerinden yapılır.
Shopify Custom Pixel ve Sandbox
Custom Pixel Lax sandbox iframe içinde çalışır. Sandbox kısıtları8:
- Tanımlı global’ler:
console, timer API’leri (setTimeout,clearTimeout,setInterval,clearInterval),fetchile birlikteHeaders/Request/Response, Web Pixels API’den deconstruct edilmişanalytics,browserveinit. Liste dışındaki non-language global’ler herhangi bir zamandaundefined’a çekilebilir; Shopify garantilemiyor8. - Erişilemez:
window.parent,window.top,document, üçüncü taraf script yükleme, çoğu DOM API.
Sandbox bilinçli olarak izole; storefront’a doğrudan erişim güvenlik nedeniyle kapalı. Veri akışı için Shopify’ın resmi yolu:
analytics.subscribe("checkout_completed", (event) => {
fetch("https://gateway.example.com/_track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "tracking.purchase",
eventId: event.id,
payload: event.data,
}),
});
});
Storefront’tan gerçek URL veya custom context taşımak için URL anatomisi yazısındaki event.context.window.location.href pattern’i geçerli.
Checkout UI Extensions’da custom JavaScript ve DOM manipulation desteklenmez; postMessage zaten gündeme gelmiyor.
Embed Widget → Parent Analytics
Genelde üçüncü taraf widget’lar iframe olarak yüklenir; çoğu zaten postMessage kullanır ama event isimleri ve payload yapıları vendor’a göre farklı.
Calendly. Resmi event listesi var9: calendly.profile_page_viewed, calendly.event_type_viewed, calendly.date_and_time_selected, calendly.event_scheduled. Parent listener:
window.addEventListener("message", (event) => {
if (!event.origin.endsWith(".calendly.com")) return; // resmi pattern; vendor ownership doğrulanmış
if (event.data.event !== "calendly.event_scheduled") return;
dataLayer.push({
event: "meeting_scheduled",
meeting_uri: event.data.payload?.event?.uri,
});
});
Typeform. form-submit event’i postMessage ile gelir ama payload yalnızca formId ve responseId içerir; form alanlarının içeriği parent’a aktarılmaz10. Conversion tracking için yeterli, lead enrichment için Typeform Webhook gerekir; tam akış Typeform dönüşüm takibi rehberinde.
Diğer chat/widget vendor’ları. Resmi event dokümanı yoksa varsayma; vendor güncellemesinde event ismi değişebilir (breaking change). Reverse engineering ile çıkardığın isimleri prod akışına bağlama.
event.origin doğrulamasında vendor subdomain’leri için allow-list yaklaşımı:
const VENDOR_ORIGINS = new Set([
"https://calendly.com",
"https://meetings.calendly.com",
]);
Vendor *.calendly.com gibi geniş bir alan dağıtıyorsa new URL(event.origin).hostname ile parse edip apex domain’i sertifika sahibi sayman gerekir; bu vendor güvenine bağlı karar.
React/Astro Cleanup Pattern
import { useEffect } from "react";
export function ConsentListener({ onUpdate }) {
useEffect(() => {
if (typeof window === "undefined") return; // SSR guard
const handler = (event) => {
if (event.origin !== "https://cmp.example.com") return;
if (event.data?.type !== "tracking.consent.update") return;
onUpdate(event.data.payload);
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [onUpdate]);
return null;
}
Yaygın hatalar:
- Cleanup’sız listener. SPA route geçişinde aynı handler birden fazla kez bağlanır; tek event 3-5 kez işlenir, GA4’e duplicate gönderilir.
- Inline arrow function ile addEventListener.
removeEventListenerreferans bulamaz, cleanup başarısız. - Yanlış dependency array.
[onUpdate]değişmiyorsa boş array kullanılabilir; her render’da listener yeniden bağlanması istenmez. - SSR’da
windowundefined. Astro client:load veya Next.jsuseEffectdışı koddatypeof windowguard’ı zorunlu.
Güvenlik Checklist
targetOriginhassas payload’da exact origin;'*'sadece public sinyallerdeevent.originexact match,event.sourcereferans kontrolü- Subdomain validation
new URL().hostname+ allow-list,endsWithkullanma - Punycode normalize,
nullorigin reject - Envelope schema:
typenamespace,eventIddedup,versionfield - Payment/fintech’te HMAC veya JWT-style signature
- Sensitive data (token, kart numarası, PII, KVKK kapsamı kişisel veri) postMessage payload’una konmaz; gateway’e POST ile gider
- Listener cleanup zorunlu
- CSP
frame-ancestorsile parent embed kısıtla;frame-srcile sayfanın hangi iframe’leri embed edebileceğini kısıtla11 <iframe sandbox="allow-scripts allow-same-origin">minimum gerekli izinle- postMessage TLS ile network seviyesinde korunur ama JS context’te plaintext; aynı sayfadaki başka script’ler dinleyebilir
Modern Alternatifler
| API | Senaryo | Cross-origin | Notlar |
|---|---|---|---|
postMessage | Cross-origin iframe/popup/window | Evet | Tracking pratiğinde baskın |
BroadcastChannel | Aynı origin’deki sekme/iframe/worker | Hayır | API daha temiz, ayrı window ref gerektirmez |
MessageChannel | İki port arasında private kanal | Evet | postMessage üzerine kurulur, izole channel |
| Service Worker postMessage | SW ile sayfa arasında | Same-origin | iframe communication için pratik değil |
| Custom Events | Aynı document içinde component sync | N/A | iframe’siz, tek pencere |
| Storage Access API | Cross-site iframe first-party cookie erişimi | Evet | Cookie blocking sertleştikçe postMessage gerekliliğini artırır12 |
Storage Access API, iframe içindeki vendor script’lerinin parent’ın first-party cookie’lerine erişim talep edebilmesini sağlıyor. Safari ITP ve Firefox ETP cross-site cookie’leri default engellediği için, vendor SDK’sı cookie okumak yerine postMessage ile veri talep etmek zorunda kalıyor; postMessage’in tracking’deki rolü azalmıyor, aksine güçleniyor.
Bir Sonraki Adım
postMessage tracking akışlarının tarayıcı içindeki köprüsü. Köprünün karşı yakası server; iframe içinden parent’a gelen consent veya purchase event’i, parent’tan da kendi gateway’ine gitmeli. Server-side gateway tasarımı, CNAME cloaking riski, path-based proxy vs subdomain pattern’i tracking için URL anatomisi yazısında. Consent Mode v2 default state ve update flush mekaniği için ayrı bir yazı yolda.
Footnotes
- Same-origin policy. MDN web docs ↩
- Window.postMessage(). MDN web docs ↩ ↩2
- Structured clone algorithm. MDN web docs ↩
- Cookiebot Google Consent Mode v2 implementation. Cookiebot docs ↩
- Consent Mode v2 default state and updates. Google Developers ↩
- Stripe.js API Reference confirmPayment. Stripe docs ↩
- Paddle.js eventCallback. Paddle docs ↩
- Web Pixels API sandbox. Shopify Dev ↩ ↩2
- Calendly Embed Event Listeners. Calendly Developer ↩
- Embed SDK form-submit event. Typeform Developer ↩
- CSP frame-ancestors vs frame-src. MDN web docs ↩
- Storage Access API. MDN web docs ↩
- 01 Sadece
event.originexact match yetmiyor;event.source !== iframe.contentWindowkontrolü multi-iframe spoof riskini kapatıyor - 02 Stripe Elements, Paddle Overlay ve İyzico kendi SDK abstraction'larını kullanır; geliştirici raw postMessage yazmaz, custom 3DS iframe entegrasyonu istisna
- 03 Shopify Custom Pixel sandbox
console, timer API'leri,fetchve Web Pixels API (analytics,browser,init,customerPrivacy) ile sınırlı;window.parent.postMessageçağrısı yok, veri akışıanalytics.subscribe()+fetch(keepalive: true) ile gateway'e - 04 Consent forwarding'de race condition: default state
denied, iframe ready handshake'i ve parent buffer olmadan ilk event'ler yanlış consent state ile gidiyor - 05 Purchase event gibi kritik tracking'de
eventId+ dedup buffer olmadan duplicate conversion kaçınılmaz
+ Aynı origin'deki iki sekme arasında postMessage mi BroadcastChannel mi?
Aynı origin için BroadcastChannel daha temiz. Tek satırlık API, ayrı window referansı gerektirmez, aynı origin'deki tüm sekme/iframe/worker'lara broadcast eder. postMessage'ı cross-origin senaryolarda (iframe parent, popup opener) sakla. BroadcastChannel safari'de partial support geçmişi vardı, 2026 itibarıyla evergreen tarayıcılarda stabil.
+ postMessage payload'ı encrypt mi?
Hayır. Mesaj JS context içinde plaintext. HTTPS yalnızca network seviyesinde koruma sağlar; aynı sayfada yüklü başka bir script window.addEventListener('message', ...) ile mesajı dinleyebilir. Hassas veri (token, kart, PII) postMessage payload'una konmaz. Gerçekten sender'ı kanıtlamak gerekiyorsa envelope'a nonce + HMAC veya JWT-style signature eklenir.
+ Stripe Elements'ten payment success event'ini postMessage ile mi alıyorum?
Hayır. Stripe.js SDK stripe.confirmPayment() Promise'i ve return_url redirect pattern'i sunar. Stripe iframe'inden parent'a doğrudan postMessage expose edilmez, implementation detail olarak kapalı. Aynı şey Paddle Overlay (Paddle.Initialize({ eventCallback })) ve İyzico Checkout Form (callbackUrl + webhook + getAuthResponse) için geçerli. Raw postMessage gerektiren senaryo kendi geliştirdiğin custom 3DS iframe entegrasyonudur.
+ Cookiebot iframe içinde mi çalışıyor?
Hayır. Cookiebot script tag olarak yüklenir, parent sayfayla aynı window'da çalışır, gtag('consent', 'update', ...) ve dataLayer üzerinden iletişim kurar. iframe + postMessage akışı OneTrust'in AMP gibi özel senaryolarında veya kendi custom consent banner'ını iframe içinde barındırdığın durumlarda devreye girer. CMP seçimi postMessage tasarım kararını belirler.
+ React'ta listener cleanup yapmazsam ne olur?
SPA route geçişlerinde aynı handler birden fazla kez bağlanır. Sonuç: tek bir consent.update mesajı 3-5 kez işlenir, GA4'e duplicate event gider, purchase tracking'de gelir iki katına çıkar (raporda; gerçekte değil). useEffect cleanup return'ünde removeEventListener zorunlu. Handler inline arrow function olursa removeEventListener referans bulamaz, named function veya useCallback referansı gerekir.
+ Custom Pixel'den postMessage göndermek mümkün mü?
Shopify Custom Pixel Lax sandbox içinde çalışır; resmi olarak izin verilen global'ler console, timer API'leri, fetch ile birlikte Headers/Request/Response, ve Web Pixels API'den deconstruct edilmiş analytics, browser, init (ve customerPrivacy). Doğrudan window.parent veya top.window erişimi yok. Liste dışı global'ler herhangi bir zamanda undefined'a çekilebilir. Storefront'a veri ulaştırmanın resmi yolu fetch ile kendi gateway'ine event göndermek; theme'den pixel'e veri akışı için Shopify.analytics.publish(eventName, payload) + pixel tarafında analytics.subscribe(eventName, ...).