<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Koray Yalçın</title>
    <description>The latest articles on DEV Community by Koray Yalçın (@korayyalcin).</description>
    <link>https://dev.to/korayyalcin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4198093%2F9a712aee-d5b9-4e7a-9c95-3a50c8038d83.png</url>
      <title>DEV Community: Koray Yalçın</title>
      <link>https://dev.to/korayyalcin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/korayyalcin"/>
    <language>en</language>
    <item>
      <title>Lead entegrasyonlarında hata yönetimi: Hata sınıflandırma, jitter'lı retry, idempotency, dead-letter ve replay</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Wed, 11 Nov 2026 16:23:40 +0000</pubDate>
      <link>https://dev.to/korayyalcin/lead-entegrasyonlarinda-hata-yonetimi-hata-siniflandirma-jitterli-retry-idempotency-5b65</link>
      <guid>https://dev.to/korayyalcin/lead-entegrasyonlarinda-hata-yonetimi-hata-siniflandirma-jitterli-retry-idempotency-5b65</guid>
      <description>&lt;p&gt;Reklamdan CRM'e uzanan bir lead akışında en pahalı hata, gürültü çıkaran hata değildir. En pahalı hata, hiç kimsenin fark etmediği hatadır. Bir webhook 500 döner, otomasyon aracı "başarısız" diye küçük bir kırmızı ikon gösterir, kimse bakmaz; üç gün sonra satış ekibi "bu kampanyadan neden hiç lead gelmedi" diye sorar. Formu dolduran kişi o sırada çoktan başka bir projeyle görüşmeye başlamıştır.&lt;/p&gt;

&lt;p&gt;Ben Koray Yalçın; lead yönetimi, CRM ve dijital pazarlama altyapıları üzerine, özellikle gayrimenkul ve PropTech tarafında çalışıyorum. Bu yazıda hata yönetimini bir "try/catch alışkanlığı" olarak değil, bir &lt;strong&gt;tasarım kararı&lt;/strong&gt; olarak ele alacağım: hatalar nasıl sınıflandırılır, ne zaman ve nasıl yeniden denenir, aynı lead'in iki kez oluşması nasıl engellenir, kurtarılamayan mesajlar nereye gider ve sonradan nasıl güvenle yeniden oynatılır. Örnekler TypeScript ve PostgreSQL ile, ama fikirler dil bağımsız.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problemi doğru tanımlamak: "Hata" tek bir şey değil
&lt;/h2&gt;

&lt;p&gt;Bir lead entegrasyonunda tipik zincir şöyledir: reklam platformu (Meta Lead Ads, Google lead form uzantısı) veya landing page formu bir olay üretir, bu olay sizin alıcı servisinize gelir, servis veriyi doğrular ve zenginleştirir, ardından CRM'e yazar ve son olarak bildirim ya da WhatsApp/SMS gibi bir ilk temas adımını tetikler.&lt;/p&gt;

&lt;p&gt;Bu zincirin her halkasında farklı türde hatalar çıkar ve hepsine aynı tepkiyi vermek iki kötü sonuçtan birine yol açar: ya her şeyi sonsuza kadar yeniden denersiniz ve kalıcı hatalar kuyruğu tıkar, ya da ilk hatada vazgeçersiniz ve geçici bir ağ kesintisi yüzünden lead kaybedersiniz.&lt;/p&gt;

&lt;p&gt;Bu yüzden ilk iş, hataları davranışa göre sınıflandırmaktır. Ben pratikte dört sınıf kullanıyorum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Geçici (transient):&lt;/strong&gt; Zaman aşımı, bağlantı kopması, 502/503/504, hız sınırı (429). Aynı istek bir süre sonra büyük ihtimalle başarılı olur.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kalıcı veri hatası (permanent-data):&lt;/strong&gt; Zorunlu alan eksik, telefon biçimi çözülemiyor, CRM bir alanı reddediyor (400/422). Aynı veriyle yüz kez denemek sonucu değiştirmez; verinin düzeltilmesi gerekir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kalıcı yapılandırma hatası (permanent-config):&lt;/strong&gt; Süresi dolmuş token (401), yetki eksikliği (403), silinmiş bir pipeline aşaması ya da alan. Veri doğru, ama sistem yanlış ayarlı. Düzeltildiğinde tüm bekleyen mesajlar tekrar oynatılabilir olmalıdır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Belirsiz (unknown):&lt;/strong&gt; İstek gitti, ama cevap gelmeden bağlantı koptu. İşlem karşı tarafta gerçekleşmiş olabilir de olmayabilir de. En tehlikeli sınıf budur, çünkü körü körüne yeniden denemek mükerrer kayıt üretebilir.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bu ayrımın kodda açıkça görünmesi gerekir; HTTP durum kodlarını iş mantığının içine dağıtmak yerine tek bir sınıflandırma fonksiyonu yazmak işi çok kolaylaştırır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// errors.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;ErrorClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"transient"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"permanent_data"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"permanent_config"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ErrorClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;retryAfterMs&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyHttp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"retry-after"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rate_limited"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"transient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`upstream_${status}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"transient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"auth_or_permission"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"permanent_config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"target_missing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"permanent_config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rejected_payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"permanent_data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`unexpected_${status}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyNetwork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestSent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// İstek hiç gönderilmediyse (DNS, bağlantı reddi) güvenle geçici sayılabilir.&lt;/span&gt;
  &lt;span class="c1"&gt;// Gönderildikten sonra kopma ise sonuç belirsizdir.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestSent&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"transient"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada önemli bir nokta var: 404'ü "veri hatası" değil "yapılandırma hatası" saydım. Lead akışlarında 404 çoğu zaman birisinin CRM'de bir aşamayı, kullanıcıyı ya da özel alanı silmesinden kaynaklanır. Bu durumda tek tek lead'leri düzeltmek yerine eşlemeyi düzeltip hepsini toplu yeniden oynatmak gerekir. Sınıflandırma, sonradan alacağınız operasyonel aksiyonu belirlemelidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Önce kalıcı kayıt, sonra işlem: Gelen olayı kaybetmemek
&lt;/h2&gt;

&lt;p&gt;Webhook alıcısında en sık gördüğüm hata, gelen isteği alır almaz CRM'e yazmaya çalışmak ve CRM yavaşsa ya da hata verirse kaynak platforma da hata döndürmektir. Bu durumda yeniden deneme sorumluluğunu tamamen kaynak platforma bırakmış olursunuz; onun yeniden deneme politikası ise sizin kontrolünüzde değildir.&lt;/p&gt;

&lt;p&gt;Daha sağlam desen şudur: gelen olayı önce kendi veritabanınıza &lt;strong&gt;ham haliyle&lt;/strong&gt; yazın, hızlıca 200 dönün, işlemi arka planda yapın.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inbound_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="kt"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;        &lt;span class="kt"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'meta_lead_ads', 'web_form', ...&lt;/span&gt;
  &lt;span class="n"&gt;source_ref&lt;/span&gt;    &lt;span class="kt"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- kaynağın kendi olay/lead kimliği&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt;   &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;       &lt;span class="kt"&gt;jsonb&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="kt"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- pending|processing|done|dead&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt;      &lt;span class="kt"&gt;int&lt;/span&gt;         &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;next_attempt_at&lt;/span&gt; &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error_class&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt;    &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;inbound_event_due&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inbound_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_attempt_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNIQUE (source, source_ref)&lt;/code&gt; kısıtı burada sessiz bir kahramandır. Kaynak platform aynı olayı iki kez gönderirse (ki webhook'lar "en az bir kez" teslim garantisiyle çalışır) ikinci kayıt veritabanı seviyesinde reddedilir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webhook handler&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="sb"&gt;`INSERT INTO inbound_event (source, source_ref, payload)
   VALUES ($1, $2, $3)
   ON CONFLICT (source, source_ref) DO NOTHING`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"meta_lead_ads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leadgen_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İmza doğrulaması (örneğin Meta webhook'larında gelen imza başlığının uygulama gizli anahtarıyla doğrulanması) bu adımdan &lt;strong&gt;önce&lt;/strong&gt; yapılmalıdır. İmzası tutmayan istek kalıcı bir hata değildir, saldırı ya da yanlış yapılandırmadır; kaydetmeden reddedin ve ayrıca loglayın.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jitter'lı exponential backoff: Yeniden denemeyi düzgün yapmak
&lt;/h2&gt;

&lt;p&gt;Geçici hatalarda yeniden deneme şarttır, ama sabit aralıklı ya da anında yeniden deneme iki sorun çıkarır. Birincisi, karşı sistem zaten zorlanıyorsa onu daha da boğarsınız. İkincisi, bir kesintiden sonra biriken bütün mesajlar aynı saniyede tekrar denenir ve "thundering herd" dediğimiz ani yük oluşur.&lt;/p&gt;

&lt;p&gt;Çözüm, katlanarak büyüyen bekleme süresine rastgelelik (jitter) eklemektir. Ben genellikle "full jitter" yaklaşımını kullanıyorum: bekleme süresi sıfır ile o denemenin üst sınırı arasında rastgele seçilir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// backoff.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;BASE_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CAP_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// tek bekleme en fazla 30 dk&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;MAX_ATTEMPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nextDelayMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAfterMs&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CAP_MS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;BASE_MS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;jittered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Karşı taraf Retry-After verdiyse ondan erken deneme&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jittered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAfterMs&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buradaki sayılar örnek değerlerdir; doğru değerler sizin iş gereksiniminize bağlıdır. Lead akışında belirleyici soru şudur: &lt;strong&gt;bir lead en geç ne kadar sürede satış ekibinin önüne düşmeli?&lt;/strong&gt; Gayrimenkulde ilk temas hızı satış sonucunu doğrudan etkilediği için, ben yeniden deneme penceresini saatlerle değil dakikalarla düşünmeyi ve pencere dolmadan önce bir insanı uyarmayı tercih ediyorum. Yeniden deneme sistemi lead'i "sonunda" teslim ederse ama bu altı saat sürerse, teknik olarak başarılı, ticari olarak başarısız olmuşsunuzdur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker: Sınıfa göre karar veren tek döngü
&lt;/h2&gt;

&lt;p&gt;Arka plan işçisi, vadesi gelen olayları alır, işler ve sonucu hata sınıfına göre kaydeder. Birden fazla işçi aynı anda çalışabileceği için satırları &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; ile kilitlemek, aynı olayın iki işçi tarafından aynı anda işlenmesini engeller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sb"&gt;`UPDATE inbound_event SET status = 'processing'
     WHERE id IN (
       SELECT id FROM inbound_event
       WHERE status = 'pending' AND next_attempt_at &amp;lt;= now()
       ORDER BY next_attempt_at
       LIMIT 20
       FOR UPDATE SKIP LOCKED
     )
     RETURNING *`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processLead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// doğrula, normalize et, CRM'e yaz&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InboundEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"transient"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MAX_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nextDelayMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="sb"&gt;`UPDATE inbound_event
       SET status='pending', attempts=$2, next_attempt_at=now() + ($3 || ' milliseconds')::interval,
           last_error_class=$4, last_error=$5
       WHERE id=$1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;moveToDeadLetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Belirsiz" sınıfını yeniden denenebilir saydığıma dikkat edin. Bunu güvenle yapabilmemin tek sebebi, bir sonraki bölümdeki idempotency katmanı. O katman yoksa belirsiz hataları otomatik yeniden denemek yerine insan incelemesine göndermek daha doğrudur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency: Aynı lead'i iki kez yaratmamak
&lt;/h2&gt;

&lt;p&gt;Belirsiz hata senaryosunu düşünün: CRM'e "lead oluştur" isteğini gönderdiniz, CRM kaydı oluşturdu, ama cevap size ulaşmadan bağlantı koptu. Yeniden denerseniz CRM'de aynı kişi iki kez görünür; iki farklı danışmana atanabilir, iki kez aranabilir. Bu, müşterinin gözünde kurumun düzensiz görünmesinin en hızlı yoludur.&lt;/p&gt;

&lt;p&gt;Bunu engellemenin üç yolu var ve genellikle en az ikisini birlikte kullanıyorum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Karşı sistemin idempotency desteği:&lt;/strong&gt; Bazı API'ler bir idempotency anahtarı başlığı kabul eder; aynı anahtarla gelen ikinci istek yeni kayıt oluşturmaz, ilk sonucu döner. Varsa mutlaka kullanın ve anahtarı &lt;code&gt;source + source_ref&lt;/code&gt; gibi deterministik bir değerden türetin, rastgele üretmeyin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harici kimlik ile upsert:&lt;/strong&gt; CRM'de "harici kaynak kimliği" gibi benzersiz bir özel alan tanımlayın ve "oluştur" yerine "bu harici kimlikle varsa güncelle, yoksa oluştur" mantığıyla yazın.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yerel eşleme tablosu:&lt;/strong&gt; Hangi olayın hangi CRM kaydına dönüştüğünü kendi tarafınızda tutun.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crm_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;      &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source_ref&lt;/span&gt;  &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;crm_object&lt;/span&gt;  &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- 'lead', 'contact', 'deal'&lt;/span&gt;
  &lt;span class="n"&gt;crm_id&lt;/span&gt;      &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crm_object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;upsertLeadInCrm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InboundEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NormalizedLead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findCrmLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"lead"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crm_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Belirsiz bir önceki denemeden kalmış olabilir: önce CRM'de harici kimlikle ara&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;searchByExternalId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${ev.source}:${ev.source_ref}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;crmId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createLead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;external_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`${ev.source}:${ev.source_ref}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`${ev.source}:${ev.source_ref}`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveCrmLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"lead"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crmId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crmId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu katman, kişi düzeyindeki tekilleştirmenin (aynı kişinin farklı formlardan iki kez gelmesi) yerine geçmez. O ayrı bir problemdir ve eşleşme kuralları gerektirir. Buradaki amaç daha dardır: &lt;strong&gt;aynı olayın&lt;/strong&gt; iki kez işlenmesini engellemek.&lt;/p&gt;

&lt;p&gt;Zincirin sonraki adımları için de aynı ilke geçerlidir. Lead CRM'e yazıldıktan sonra tetiklenen WhatsApp ilk mesajı, danışman bildirimi ya da görev oluşturma adımlarının her biri kendi idempotency anahtarına sahip olmalıdır. Aksi halde CRM adımı başarılı olup bildirim adımı başarısız olduğunda tüm akışı yeniden denemek, müşteriye ikinci kez "merhaba" mesajı gitmesine yol açar. Bu yüzden ben her yan etkiyi ayrı bir adım olarak kaydediyor ve yalnızca tamamlanmamış adımları yeniden deniyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead-letter tablosu: Kurtarılamayanı kaybetmeden ayırmak
&lt;/h2&gt;

&lt;p&gt;Kalıcı hatalar ve deneme hakkı biten olaylar silinmemeli, ayrı bir yere taşınmalıdır. Bu "dead-letter" alanı bir çöp kutusu değil, bir &lt;strong&gt;iş kuyruğu&lt;/strong&gt;dur: her satırın bir sahibi, bir sebebi ve bir çözüm durumu olmalıdır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dead_letter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="kt"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;inbound_event_id&lt;/span&gt; &lt;span class="kt"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nf"&gt;inbound_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_class&lt;/span&gt;     &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_message&lt;/span&gt;   &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_detail&lt;/span&gt;    &lt;span class="kt"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt;        &lt;span class="kt"&gt;int&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dead_at&lt;/span&gt;         &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolution&lt;/span&gt;      &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- NULL | 'replayed' | 'fixed_manually' | 'discarded'&lt;/span&gt;
  &lt;span class="n"&gt;resolved_by&lt;/span&gt;     &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolved_at&lt;/span&gt;     &lt;span class="kt"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;note&lt;/span&gt;            &lt;span class="kt"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moveToDeadLetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InboundEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IntegrationError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`UPDATE inbound_event SET status='dead', attempts=$2,
                   last_error_class=$3, last_error=$4 WHERE id=$1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`INSERT INTO dead_letter (inbound_event_id, error_class, error_message, error_detail, attempts)
                   VALUES ($1,$2,$3,$4,$5)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uyarı politikası sınıfa göre değişmelidir. Tek bir &lt;code&gt;permanent_data&lt;/code&gt; hatası için bütün ekibi uyandırmaya gerek yoktur; günlük bir özet yeterlidir. Ama tek bir &lt;code&gt;permanent_config&lt;/code&gt; hatası, muhtemelen o andan sonra gelen &lt;strong&gt;bütün&lt;/strong&gt; lead'lerin aynı yere düşeceği anlamına gelir; bu hemen bir insana ulaşmalıdır.&lt;/p&gt;

&lt;p&gt;Kalıcı veri hatalarında bile lead'in kendisini tamamen kaybetmemek mümkündür. Telefon numarası çözülemeyen bir kayıt, e-postası geçerliyse "eksik veri" etiketiyle CRM'e yine de yazılabilir ve bir danışmana manuel kontrol görevi açılabilir. Teknik olarak hatalı olan veri, ticari olarak hâlâ değerli bir taleptir. Hangi alanların gerçekten zorunlu olduğuna, satış ekibiyle birlikte karar vermek gerekir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay: Düzelttikten sonra güvenle yeniden oynatmak
&lt;/h2&gt;

&lt;p&gt;Yapılandırma hatasını düzelttiniz, diyelim ki süresi dolan CRM token'ını yenilediniz. Şimdi dead-letter'daki yüzlerce olayı geri almanız gerekiyor. Bunu güvenli yapmak için birkaç kural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replay, normal işleme yolunu kullanmalı.&lt;/strong&gt; Ayrı bir "toplu import" scripti yazmak yerine olayları tekrar &lt;code&gt;pending&lt;/code&gt; durumuna alın. Böylece aynı doğrulama, aynı idempotency ve aynı izleme devreye girer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtrelenebilir olmalı.&lt;/strong&gt; Sınıfa, kaynağa ve zaman aralığına göre seçim yapabilmelisiniz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kademeli olmalı.&lt;/strong&gt; Önce küçük bir parti, sonuçları kontrol, sonra geri kalanı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;İz bırakmalı.&lt;/strong&gt; Kim, ne zaman, hangi gerekçeyle yeniden oynattı.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Örnek: belirli bir zaman aralığındaki yapılandırma hatalarını ilk 50 kayıtla yeniden kuyruğa al&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dl_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inbound_event_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dead_letter&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inbound_event&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inbound_event_id&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolution&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'permanent_config'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'meta_lead_ads'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dead_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dead_at&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;requeue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inbound_event&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_attempt_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inbound_event_id&lt;/span&gt;
  &lt;span class="k"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dead_letter&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'replayed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$4&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;picked&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dl_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replay sırasında unutulmaması gereken bir ayrıntı daha var: bazı adımlar zamana duyarlıdır. Üç gün önce gelmiş bir lead'e otomatik "Formunuzu aldık, hemen arıyoruz" mesajı göndermek yanlış bir beklenti yaratır. Bu yüzden ilk temas adımı, olayın &lt;strong&gt;orijinal alınma zamanına&lt;/strong&gt; bakmalı ve belli bir süreyi aşan kayıtlarda otomatik mesaj yerine danışmana öncelikli bir arama görevi açmalıdır. Reklam platformlarına geri dönüşüm gönderen adımlarda da platformların olay zamanı için kabul ettiği geriye dönük sınırlar vardır; sınırı aşan olayları göndermeye çalışmak yerine raporlamak gerekir.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzleme: Hatanın değil, sessizliğin alarmı
&lt;/h2&gt;

&lt;p&gt;Bütün bu yapı kurulduğunda bile bir tür hata hiçbir tabloya düşmez: &lt;strong&gt;hiç gelmeyen olay.&lt;/strong&gt; Reklam platformundaki webhook aboneliği düşmüşse, form sayfasında bir JavaScript hatası gönderimi engelliyorsa ya da bir DNS değişikliği endpoint'inizi erişilmez kılmışsa, sisteminiz kusursuz biçimde "hata yok" raporlar.&lt;/p&gt;

&lt;p&gt;Bu yüzden ben izlemeyi üç katmanda kuruyorum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hacim sapması:&lt;/strong&gt; Her kaynak için beklenen saatlik lead aralığını geçmiş verilerden çıkarıp, belirgin bir düşüşte uyarı. Kampanya bütçesi açıkken bir kaynaktan uzun süre hiç olay gelmemesi başlı başına bir alarmdır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutabakat:&lt;/strong&gt; Belirli aralıklarla reklam platformunun kendi API'sinden o dönemde oluşan lead kimliklerini çekip &lt;code&gt;inbound_event&lt;/code&gt; tablosundaki &lt;code&gt;source_ref&lt;/code&gt; değerleriyle karşılaştırmak. Fark varsa eksik olanları çekip normal yoldan işlemek.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kuyruk sağlığı:&lt;/strong&gt; &lt;code&gt;pending&lt;/code&gt; durumunda en eski olayın yaşı, dead-letter'daki çözülmemiş kayıt sayısı ve sınıflara göre dağılımı.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Kuyruk sağlığı için tek sorgu&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;received_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inbound_event&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;oldest_pending_age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="cm"&gt;(*) FROM dead_letter WHERE resolution IS NULL)                         AS open_dead_letters,
  (SELECT jsonb_object_agg(error_class, c) FROM (
      SELECT error_class, count(*)&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dead_letter&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;resolution&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;error_class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;open_by_class&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mutabakat adımı, bütün tasarımın sigortasıdır. Webhook'lar, kuyruklar ve yeniden denemeler ne kadar iyi kurulursa kurulsun, "kaynakta var, bizde yok" sorusunu düzenli soran bir süreç olmadan sistemin güvenilir olduğunu iddia edemezsiniz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Otomasyon araçlarıyla çalışıyorsanız
&lt;/h2&gt;

&lt;p&gt;Her ekip bu altyapıyı sıfırdan kod olarak yazmaz; çoğu akış Make, n8n ya da Zapier gibi araçlarda kuruludur. Aynı ilkeler orada da uygulanabilir: hata yolu (error route) olmayan bir senaryo bırakmayın, hata yolunda olayı ham haliyle bir tabloya ya da sayfaya yazın, kalıcı ve geçici hataları farklı dallara ayırın, CRM adımında "oluştur" yerine harici kimlikle arama + güncelleme kullanın ve haftalık bir mutabakat kontrolü planlayın. Aracın kendi yeniden deneme ayarlarına güvenmek yerine, en azından kurtarılamayan olayların görünür bir yerde biriktiğinden emin olun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık sorulan sorular (SSS)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Her hatayı yeniden denemek neden kötü bir fikir?&lt;/strong&gt;&lt;br&gt;
Çünkü kalıcı hatalar yeniden denemeyle düzelmez. Eksik zorunlu alanı olan bir kaydı on iki kez denemek yalnızca kuyruğu meşgul eder, gerçek geçici hataların işlenmesini geciktirir ve insanın sorunu fark etmesini erteler. Kalıcı hatayı hemen dead-letter'a alıp sahibine bildirmek daha hızlı çözüm getirir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jitter gerçekten gerekli mi, sabit bekleme yetmez mi?&lt;/strong&gt;&lt;br&gt;
Az trafikte fark hissedilmez. Ama bir CRM ya da API kesintisi sonrası yüzlerce mesaj aynı anda beklemeye girer; sabit aralıkla hepsi aynı anda tekrar denenir ve sistem toparlanırken yeniden boğulur. Rastgelelik bu yükü zamana yayar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kaynak platform zaten yeniden deniyorsa benim ayrıca retry yazmama gerek var mı?&lt;/strong&gt;&lt;br&gt;
Evet. Kaynağın yeniden denemesi yalnızca sizin endpoint'inize ulaşma sorununu çözer; sizin CRM'e, mesajlaşma servisine ya da reklam platformuna yaptığınız çağrıların başarısızlığını çözmez. Ayrıca kaynağın politikası değişebilir ve sizin kontrolünüzde değildir. Olayı önce kendi tablonuza yazmak, sorumluluğu sizin tarafınıza alır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency anahtarını nasıl seçmeliyim?&lt;/strong&gt;&lt;br&gt;
Deterministik olmalı ve olayın kendisinden türetilmelidir: kaynak adı ve kaynağın verdiği benzersiz kimlik iyi bir başlangıçtır. Her denemede rastgele bir anahtar üretirseniz, anahtarın hiçbir koruma sağlamaz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead-letter'daki kayıtları ne kadar tutmalıyım?&lt;/strong&gt;&lt;br&gt;
Çözülene kadar mutlaka; çözüldükten sonra ise kişisel veri içerdikleri için şirketinizin veri saklama politikasına tabi olmalıdırlar. Çözülmüş kayıtların ham yükünü belirli bir süre sonra silip yalnızca istatistiksel özeti tutmak iyi bir dengedir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bu tasarım küçük bir ekip için fazla mı?&lt;/strong&gt;&lt;br&gt;
Tamamını ilk günden kurmak gerekmez. Benim önerdiğim öncelik sırası şöyle: önce gelen olayı ham haliyle kalıcı yazmak, sonra harici kimlikle upsert, ardından dead-letter ve uyarı, en son mutabakat. İlk iki adım bile sessiz lead kaybının en yaygın kaynaklarını kapatır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapanış
&lt;/h2&gt;

&lt;p&gt;Lead entegrasyonlarında hata yönetimi, bir altyapı detayı gibi görünse de doğrudan satışa dokunan bir konudur. Kaybolan her olay, bir insanın "bilgi almak istiyorum" demesinin cevapsız kalması anlamına gelir. Hataları sınıflandırmak, yeniden denemeyi ölçülü yapmak, idempotency ile mükerrer kaydı engellemek, kurtarılamayanı görünür bir kuyrukta toplamak ve düzenli mutabakatla sessizliği yakalamak; bu beş adım birlikte, "çalışıyor gibi görünen" bir akışı gerçekten güvenilir bir akışa dönüştürür.&lt;/p&gt;

&lt;p&gt;Lead yönetimi ve CRM altyapısı üzerine bu tür uygulama notlarını paylaşmaya devam edeceğim. Kendi akışınızda en çok hangi hata sınıfıyla uğraştığınızı yorumlarda yazarsanız, bir sonraki yazılarda o senaryoları daha ayrıntılı ele alabilirim.&lt;/p&gt;

&lt;p&gt;— Koray Yalçın&lt;/p&gt;

</description>
      <category>backend</category>
      <category>crm</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Çerez izni (cookie consent) koda dökmek: İzin durumu modeli, script kapısı, Consent Mode v2 ve sunucu tarafına taşıma</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Wed, 11 Nov 2026 08:20:11 +0000</pubDate>
      <link>https://dev.to/korayyalcin/cerez-izni-cookie-consent-koda-dokmek-izin-durumu-modeli-script-kapisi-consent-mode-v2-ve-27o7</link>
      <guid>https://dev.to/korayyalcin/cerez-izni-cookie-consent-koda-dokmek-izin-durumu-modeli-script-kapisi-consent-mode-v2-ve-27o7</guid>
      <description>&lt;p&gt;Konut projeleri için landing page ve CRM altyapısı kurarken en sık karşılaştığım teknik borçlardan biri çerez izni katmanıdır. Çoğu sitede bir banner vardır, "Kabul et" ve "Reddet" düğmeleri vardır; ama arka planda pikseller banner'dan önce yüklenmiş, reddetme seçeneği yalnızca banner'ı kapatıyor, izin durumu hiçbir yerde kayıt altına alınmıyor ve sunucu tarafı olaylar (Meta CAPI, Google offline dönüşüm) izinden tamamen habersiz çalışıyor olur.&lt;/p&gt;

&lt;p&gt;Ben Koray Yalçın; lead yönetimi, CRM ve dijital pazarlama altyapıları üzerine çalışıyorum. Bu yazıda çerez iznini bir "banner tasarımı" meselesi olarak değil, bir &lt;strong&gt;veri akışı ve durum yönetimi&lt;/strong&gt; problemi olarak ele alacağım: izin durumu nasıl modellenir, scriptler nasıl kapı arkasında tutulur, Google Consent Mode v2 nereye oturur, izin bilgisi sunucu tarafına ve CRM'e nasıl taşınır ve tüm bunlar nasıl test edilir.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Not: Bu yazı teknik bir uygulama rehberidir, hukuki görüş değildir. KVKK, GDPR ve ePrivacy kapsamındaki yükümlülükleriniz için hukuk danışmanınızla çalışın.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problemi doğru tanımlamak
&lt;/h2&gt;

&lt;p&gt;Çerez izni üç ayrı soruya cevap vermelidir:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ne zaman?&lt;/strong&gt; Hangi script, hangi izin verilmeden önce çalışabilir?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ne kadar?&lt;/strong&gt; Kullanıcı hangi kategorilere, hangi metin sürümüne, ne zaman izin verdi?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nereye kadar?&lt;/strong&gt; Bu izin bilgisi tarayıcıda mı kalıyor, yoksa sunucu tarafı olaylara ve CRM kaydına da ulaşıyor mu?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Çoğu hazır CMP (Consent Management Platform) ilk soruyu kısmen çözer. İkinci ve üçüncü soru ise neredeyse her zaman sizin kodunuza kalır. Özellikle form doldurup lead'e dönüşen bir ziyaretçi için, çerez izni ile pazarlama iletişim izninin (ETK/İYS kapsamındaki ticari ileti izni) &lt;strong&gt;farklı şeyler&lt;/strong&gt; olduğunu baştan netleştirmek gerekir. Çerez izni tarayıcıdaki ölçüm ve reklam teknolojileriyle ilgilidir; ticari ileti izni ise kişiye mesaj gönderip gönderemeyeceğinizle. İkisini tek bir onay kutusunda birleştirmek hem hukuken hem teknik olarak sorun çıkarır.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzin durumu modeli: Kategoriler ve sürüm
&lt;/h2&gt;

&lt;p&gt;İlk adım, izin durumunu tek bir boolean olarak değil, kategorilere ayrılmış ve sürümlenmiş bir nesne olarak tutmaktır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// consent-state.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CONSENT_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-01"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// çerez metninin sürümü&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CATEGORIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;necessary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// oturum, güvenlik, yük dengeleme&lt;/span&gt;
  &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// GA4, PostHog vb.&lt;/span&gt;
  &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Meta Pixel, Google Ads, retargeting&lt;/span&gt;
  &lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// canlı sohbet, harita, video gömme&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;defaultState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;CONSENT_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;decidedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// kullanıcı karar vermediyse null&lt;/span&gt;
    &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;necessary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;consentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// kanıt kaydını bağlamak için&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada üç kritik nokta var:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Varsayılan reddettir.&lt;/strong&gt; Karar verilmemiş durum ile reddedilmiş durum farklı alanlarla ayırt edilir (&lt;code&gt;decidedAt: null&lt;/code&gt;), ama ikisinde de opsiyonel kategoriler kapalıdır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sürüm alanı zorunludur.&lt;/strong&gt; Çerez politikası değiştiğinde, örneğin yeni bir reklam ortağı eklediğinizde, eski sürümle verilmiş izin yeni durumu kapsamaz. Kayıtlı sürüm güncel sürümden farklıysa banner yeniden gösterilir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;consentId&lt;/code&gt;&lt;/strong&gt; tarayıcıdaki kararı sunucudaki kanıt kaydına bağlayan anahtardır. Kişisel veri değildir; rastgele bir kimliktir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Durumu saklarken birinci taraf bir çerez tercih ediyorum (ör. &lt;code&gt;cc_state&lt;/code&gt;), çünkü sunucu da ilk istekte bu değeri okuyabilmeli. localStorage yalnızca tarayıcıdan erişilebilir; sunucu tarafı render ve edge mantığı için yetersiz kalır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"; "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cc_state="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;defaultState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="no"&gt;CONSENT_VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// politika değişti: kimliği koru, kararı sıfırla&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defaultState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consentId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;defaultState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;writeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;maxAge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// saklama süresini politikanıza göre belirleyin&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="sb"&gt;`cc_state=${value}; Max-Age=${maxAge}; Path=/; SameSite=Lax; Secure`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saklama süresi burada bir örnektir; kaç gün tutacağınız sizin politika kararınızdır ve politika metninde yazanla birebir aynı olmalıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script kapısı: İzin gelmeden hiçbir şey çalışmasın
&lt;/h2&gt;

&lt;p&gt;Sahada gördüğüm en yaygın hata, banner'ın doğru çalışması ama piksellerin &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; içinde doğrudan yüklenmesidir. Ağ sekmesini açtığınızda, kullanıcı daha hiçbir şeye tıklamadan &lt;code&gt;facebook.com/tr&lt;/code&gt; ve &lt;code&gt;googleads&lt;/code&gt; isteklerinin gittiğini görürsünüz.&lt;/p&gt;

&lt;p&gt;Çözüm, opsiyonel scriptleri tarayıcının çalıştırmayacağı bir türle işaretlemek ve izin geldiğinde etkinleştirmektir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Çalışmaz: type text/plain olduğu için tarayıcı yürütmez --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text/plain"&lt;/span&gt; &lt;span class="na"&gt;data-consent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"marketing"&lt;/span&gt;
        &lt;span class="na"&gt;data-src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://connect.facebook.net/tr_TR/fbevents.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text/plain"&lt;/span&gt; &lt;span class="na"&gt;data-consent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"analytics"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c1"&gt;// analitik başlatma kodu&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// consent-gate.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;activateScripts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'script[type="text/plain"][data-consent]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"script"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu yaklaşımın önemli bir sınırı var: izin &lt;strong&gt;geri çekildiğinde&lt;/strong&gt; zaten yüklenmiş bir scripti "geri yükleyemezsiniz". Bu yüzden geri çekmede iki şey yapıyorum: ilgili birinci taraf çerezleri siliyorum ve sayfayı yeniliyorum. Yenilenen sayfada kapı yeni duruma göre yeniden değerlendirilir. Zarif değil ama doğru davranış budur.&lt;/p&gt;

&lt;p&gt;Gömülü içerikler (YouTube videosu, harita, canlı sohbet widget'ı) için de aynı mantık geçerli: izin yoksa iframe yerine bir yer tutucu ve "Bu içeriği görmek için işlevsel çerezlere izin verin" düğmesi gösterilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Consent Mode v2 nereye oturur?
&lt;/h2&gt;

&lt;p&gt;Google etiketleri kullanıyorsanız, kendi kapınızın yanında Consent Mode sinyallerini de doğru vermelisiniz. Consent Mode v2'de reklam tarafı için &lt;code&gt;ad_storage&lt;/code&gt;, &lt;code&gt;ad_user_data&lt;/code&gt; ve &lt;code&gt;ad_personalization&lt;/code&gt;, analitik için &lt;code&gt;analytics_storage&lt;/code&gt; parametreleri kullanılır.&lt;/p&gt;

&lt;p&gt;Sıralama kritiktir: &lt;strong&gt;varsayılan durum, herhangi bir Google etiketi yüklenmeden önce&lt;/strong&gt; tanımlanmalıdır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"consent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ad_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ad_user_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ad_personalization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;analytics_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;functionality_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;personalization_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;security_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"granted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wait_for_update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kullanıcı karar verdiğinde kendi durum nesnenizi Consent Mode diline çeviren tek bir fonksiyon yazın:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toGoogleConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"granted"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"denied"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ad_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ad_user_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ad_personalization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;analytics_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;functionality_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;personalization_storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gtag&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"consent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;toGoogleConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;activateScripts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada bir mimari karar vermeniz gerekiyor: Google etiketlerini "Consent Mode'un temel uygulaması" ile mi (izin yoksa etiket hiç yüklenmez), yoksa "gelişmiş uygulama" ile mi (etiket yüklenir, izin yoksa çerezsiz sinyal gönderir) kullanacaksınız? Bu teknik bir tercih gibi görünse de aslında bir uyum kararıdır. Ben bu kararı kod değil, ayar olarak tutuyor ve hukuk ekibiyle birlikte veriyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kararı kanıtlamak: Sunucu tarafı izin kaydı
&lt;/h2&gt;

&lt;p&gt;Tarayıcıdaki çerez, kullanıcının neye izin verdiğinin &lt;strong&gt;kanıtı değildir&lt;/strong&gt;; kullanıcı onu silebilir, değiştirebilir. Bu yüzden her kararı sunucuya bir olay olarak yazıyorum. Kişisel veri tutmadan, yalnızca kararı ve bağlamını saklayan, ekleme yapılan (append-only) bir tablo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cookie_consent_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="kt"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;consent_id&lt;/span&gt;    &lt;span class="kt"&gt;UUID&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;version&lt;/span&gt;       &lt;span class="kt"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- çerez metni sürümü&lt;/span&gt;
  &lt;span class="n"&gt;choices&lt;/span&gt;       &lt;span class="kt"&gt;JSONB&lt;/span&gt;       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- {"analytics":true,...}&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;        &lt;span class="kt"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;         &lt;span class="c1"&gt;-- accept_all | reject_all | custom | withdraw&lt;/span&gt;
                &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'accept_all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'reject_all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'custom'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'withdraw'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ui_variant&lt;/span&gt;    &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;-- hangi banner tasarımı gösterildi&lt;/span&gt;
  &lt;span class="n"&gt;page_path&lt;/span&gt;     &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ua_hash&lt;/span&gt;       &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;-- tam user-agent yerine hash&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cookie_consent_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IP adresini ham haliyle saklamıyorum. İhtiyaç varsa kısaltılmış ya da tuzlu hash'lenmiş bir türev yeterlidir. &lt;code&gt;ui_variant&lt;/code&gt; alanı ise ileride "Reddet düğmesi eşit görünürlükte miydi?" sorusu geldiğinde hangi tasarımın gösterildiğini kanıtlamak için var.&lt;/p&gt;

&lt;p&gt;Tarayıcı tarafında kayıt çağrısı basit tutulur ve başarısız olsa bile kullanıcı deneyimini bozmaz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recordDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/api/consent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;keepalive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;consentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;uiVariant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ccVariant&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pagePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ağ hatası kararı geçersiz kılmaz; bir sonraki sayfa yüklemesinde yeniden dene&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  İzni sunucu tarafı olaylara taşımak
&lt;/h2&gt;

&lt;p&gt;Asıl kör nokta burasıdır. Bir ziyaretçi pazarlama çerezlerini reddedip formu doldurduğunda, CRM'iniz o lead'i "nitelikli" aşamasına aldığında Meta Conversions API'ye veya Google Ads'e offline dönüşüm göndermeye devam ediyorsa, tarayıcıda kurduğunuz bütün kapı anlamını yitirir.&lt;/p&gt;

&lt;p&gt;Bunu çözmek için form gönderiminde çerez izni anlık görüntüsünü lead kaydına ekliyorum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// form gönderiminde&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookieConsent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;consentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;capturedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CRM tarafında dışa giden her reklam olayı, tek bir kontrol fonksiyonundan geçer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_send_ad_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookie_consent&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"marketing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;consent_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"consent_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# sonradan geri çekildiyse eski anlık görüntüye güvenme&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"withdraw"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu fonksiyonu outbox işleyicisinin içinde, gönderimden hemen önce çağırmak önemli. Lead oluştuğu anda izin vardı ama üç gün sonra satış aşaması değiştiğinde kullanıcı izni geri çekmiş olabilir. Kontrolü olayın üretildiği anda değil, &lt;strong&gt;gönderildiği anda&lt;/strong&gt; yapmak bu açığı kapatır.&lt;/p&gt;

&lt;p&gt;Bir de şu ayrımı not edeyim: çerez izni, tarayıcı tarafı takip teknolojileri içindir; sunucu tarafı reklam olaylarında kişisel verinin işlenmesi ayrıca bir hukuki dayanak gerektirebilir. Teknik olarak yapabileceğiniz en temiz şey, iki izni de ayrı alanlarda tutmak ve gönderim kontrolünde ikisini birlikte değerlendirmektir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Banner'ın kendisi: Teknik açıdan dikkat edilecekler
&lt;/h2&gt;

&lt;p&gt;Tasarım bu yazının konusu değil, ama koda yansıyan birkaç kural var:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eşit seçenekler.&lt;/strong&gt; "Tümünü kabul et" ile "Tümünü reddet" aynı katmanda, benzer görünürlükte olmalı. Reddetmeyi ikinci bir ekrana saklamak hem güven hem uyum sorunudur.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erişilebilirlik.&lt;/strong&gt; Banner klavyeyle gezilebilir olmalı, odak ilk açılışta banner'a taşınmalı, &lt;code&gt;role="dialog"&lt;/code&gt; ve &lt;code&gt;aria-labelledby&lt;/code&gt; kullanılmalı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kararı değiştirebilme.&lt;/strong&gt; Sayfa altında her zaman erişilebilen bir "Çerez tercihleri" bağlantısı olmalı ve aynı modalı açmalı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performans.&lt;/strong&gt; Banner scripti küçük ve senkron olmalı; ama sayfanın ilk boyamasını engellemeyecek şekilde. Ağır bir CMP yüklüyorsanız varsayılan Consent Mode bloğunu yine de satır içi ve en üstte tutun.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test etmek: "Bana güven" yerine kanıt
&lt;/h2&gt;

&lt;p&gt;İzin katmanı, otomatik testi en çok ihmal edilen bileşenlerden biridir. Ben üç senaryoyu sürekli entegrasyon hattında çalıştırıyorum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright test örneği&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"karar öncesi pazarlama isteği gitmez"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;facebook&lt;/span&gt;\&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;\&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;googleadservices&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;doubleclick&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/proje/ornek-landing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"reddet sonrası da gitmez"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// banner'da reddet, sonra formu doldur, aynı kontrol&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"politika sürümü değişince banner yeniden açılır"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// eski sürümle cc_state çerezi yaz, sayfayı aç, banner görünür mü?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bunlara ek olarak haftalık bir mutabakat sorgusu çalıştırıyorum: son yedi günde reklam platformlarına gönderilen sunucu tarafı olayların kaç tanesi &lt;code&gt;marketing=false&lt;/code&gt; olan lead'lere aitti? Doğru kurulmuş bir sistemde bu sayı sıfır olmalıdır; sıfır değilse bir yerde kapı sızdırıyor demektir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık yapılan hatalar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pikselleri &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;'e doğrudan koyup banner'ı yalnızca görsel bir katman olarak eklemek.&lt;/li&gt;
&lt;li&gt;Consent Mode varsayılanını Google etiketinden &lt;strong&gt;sonra&lt;/strong&gt; tanımlamak.&lt;/li&gt;
&lt;li&gt;Politika değişince eski izinleri geçerli saymak; sürüm alanı tutmamak.&lt;/li&gt;
&lt;li&gt;Çerez iznini ticari ileti izniyle tek onay kutusunda birleştirmek.&lt;/li&gt;
&lt;li&gt;Sunucu tarafı reklam olaylarını izin durumundan bağımsız göndermek.&lt;/li&gt;
&lt;li&gt;Kararı yalnızca tarayıcıda tutup sunucuda kanıt kaydı oluşturmamak.&lt;/li&gt;
&lt;li&gt;Geri çekmede sayfayı yenilemeden, yüklenmiş scriptlerin "kendiliğinden duracağını" varsaymak.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sık Sorulan Sorular (SSS)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hazır bir CMP kullanıyorsam bu yazıdakileri yine de yapmam gerekir mi?&lt;/strong&gt;&lt;br&gt;
CMP genellikle banner'ı, kategori seçimini ve Consent Mode sinyallerini çözer. Ancak izin anlık görüntüsünü lead kaydına eklemek, sunucu tarafı olayları izinle kapılamak ve mutabakat sorgusu çalıştırmak çoğu zaman sizin backend'inize kalır. CMP'nin olay geri çağırmalarını (callback) kendi &lt;code&gt;applyConsent&lt;/code&gt; ve &lt;code&gt;recordDecision&lt;/code&gt; fonksiyonlarınıza bağlamanız yeterli.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kullanıcı reddederse analitik verim tamamen kaybolur mu?&lt;/strong&gt;&lt;br&gt;
Kurduğunuz uygulamaya bağlıdır. Consent Mode'un gelişmiş uygulamasında Google etiketleri çerezsiz sinyaller gönderebilir ve modelleme yapılabilir; temel uygulamada izin yoksa etiket hiç yüklenmez. Hangisinin size uygun olduğu bir uyum kararıdır, teknik ekip tek başına vermemelidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;İzin durumunu localStorage'da mı, çerezde mi tutmalıyım?&lt;/strong&gt;&lt;br&gt;
Sunucunun da okuyabilmesi için birinci taraf bir çerez daha pratiktir. Sunucu tarafı render, edge kuralları ve form gönderiminde izin anlık görüntüsü almak için bu önemli bir avantaj.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Çerez izni ile KVKK açık rızası aynı şey mi?&lt;/strong&gt;&lt;br&gt;
Hayır, teknik olarak ayrı tutulmalıdır. Çerez izni tarayıcıdaki takip teknolojileriyle, ticari ileti izni kişiye mesaj gönderimiyle, aydınlatma metni ise bilgilendirmeyle ilgilidir. Her biri ayrı alan, ayrı kayıt ve ayrı geri çekme akışı ister.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Politika metnini her güncellediğimde herkese banner'ı yeniden mi göstermeliyim?&lt;/strong&gt;&lt;br&gt;
Kapsamı değiştiren güncellemelerde (yeni kategori, yeni reklam ortağı, yeni amaç) evet; bunun için sürüm alanını artırırsınız. Yazım hatası düzeltmesi gibi kapsamı etkilemeyen değişikliklerde sürüm artırmak gerekmeyebilir; bu ayrımı politika sahibiyle birlikte yapın.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sunucu tarafı takip (server-side tagging) kurarsam çerez iznine gerek kalmaz mı?&lt;/strong&gt;&lt;br&gt;
Kalır. Sunucu tarafı takip verinin nereden gönderildiğini değiştirir, izin yükümlülüğünü ortadan kaldırmaz. Aksine, tarayıcıdaki kapıyı atlayan bir kanal olduğu için izin kontrolünü sunucu tarafında da açıkça uygulamanız gerekir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapanış
&lt;/h2&gt;

&lt;p&gt;Çerez izni doğru kurulduğunda bir "uyum maliyeti" değil, ölçüm altyapınızın güvenilirlik katmanı olur. Hangi verinin hangi izinle toplandığını bilen bir sistemde reklam platformlarına giden sinyaller daha temiz, CRM'deki lead kaydı daha açıklanabilir ve denetim sorularına verilecek cevap hazırdır. Benim önerim basit: izin durumunu sürümlü bir nesne olarak modelleyin, scriptleri kapı arkasında tutun, kararı sunucuda kayıt altına alın ve reklam olaylarını gönderildikleri anda izinle kontrol edin.&lt;/p&gt;

&lt;p&gt;— Koray Yalçın&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>marketing</category>
    </item>
    <item>
      <title>CRM'de activity modelini kurmak: Tek aktivite tablosu, durum geçişleri, kaynak tekilleştirme ve son temas</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:23:48 +0000</pubDate>
      <link>https://dev.to/korayyalcin/crmde-activity-modelini-kurmak-tek-aktivite-tablosu-durum-gecisleri-kaynak-tekillestirme-ve-son-52ae</link>
      <guid>https://dev.to/korayyalcin/crmde-activity-modelini-kurmak-tek-aktivite-tablosu-durum-gecisleri-kaynak-tekillestirme-ve-son-52ae</guid>
      <description>&lt;p&gt;Bir CRM'in en çok okunan ekranı genelde lead kartının altındaki zaman çizelgesidir: kim aradı, hangi mesaj gitti, toplantı ne zaman yapıldı, sıradaki adım ne. Satış müdürü bir fırsatı incelerken ilk oraya bakar, danışman bir müşteriyi devraldığında ilk oraya bakar. Buna rağmen birçok CRM'de bu ekranın arkasındaki veri modeli aceleyle kurulur: her kanal için ayrı bir tablo, birbirini tutmayan tarih alanları, elle girilmiş ve sonradan düzeltilmiş kayıtlar. Sonuç, "bu lead'i en son kim aradı?" gibi basit bir sorunun bile güvenilir cevabının olmamasıdır.&lt;/p&gt;

&lt;p&gt;Ben Koray Yalçın; uzun süredir lead yönetimi, CRM kurulumu ve gayrimenkul satış operasyonları üzerine çalışıyorum. Bu yazıda CRM'deki activity kavramını, yani temsilcilerin ve sistemlerin bir kayıt üzerinde yaptığı arama, mesaj, toplantı, e-posta ve görev gibi aksiyonların bütününü, bir geliştiricinin gözünden ele alıyorum: tek bir aktivite tablosu nasıl tasarlanır, telefon santrali ve WhatsApp gibi dış kaynaklardan gelen kayıtlar nasıl tekilleştirilir, planlanmış ve gerçekleşmiş aktivite nasıl ayrılır, "son temas" ve "sıradaki adım" gibi türetilmiş alanlar nasıl tutarlı tutulur ve tüm bunlar nasıl raporlanır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aktivite neden ayrı bir kavram olarak modellenmeli?
&lt;/h2&gt;

&lt;p&gt;Aktiviteyi hafife almanın tipik görüntüsü şudur: &lt;code&gt;leads&lt;/code&gt; tablosunda &lt;code&gt;last_call_at&lt;/code&gt;, &lt;code&gt;last_whatsapp_at&lt;/code&gt;, &lt;code&gt;notes&lt;/code&gt; gibi kolonlar vardır; arama kayıtları santral panelinde, mesajlar WhatsApp sağlayıcısında, toplantılar takvimde durur. Her yeni kanal, ana tabloya yeni bir kolon ve yeni bir entegrasyon kodu demektir.&lt;/p&gt;

&lt;p&gt;Bu yaklaşımın üç somut sorunu var. Birincisi, geçmiş kaybolur: &lt;code&gt;last_call_at&lt;/code&gt; sadece en son aramayı tutar, önceki beş aramanın sonucunu değil. İkincisi, kanallar arası sıralama yapılamaz: "önce mesaj mı gitti, arama mı yapıldı?" sorusu üç ayrı sisteme bakmadan cevaplanamaz. Üçüncüsü, ölçüm imkânsızlaşır: bir danışmanın günlük iş yükünü veya bir lead'e ilk dokunuşun ne kadar sürdüğünü hesaplamak için önce veriyi bir araya getirmeniz gerekir.&lt;/p&gt;

&lt;p&gt;Çözüm, aktiviteyi kendi başına bir varlık olarak modellemek ve her kanalı bu ortak modele eşlemektir. Lead, kişi, fırsat (deal) ve şirket kayıtları aktivitelerin "üzerinde gerçekleştiği" şeylerdir; aktivitenin kendisi ise ayrı bir olay kaydıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tek tablo, ortak çekirdek, türe özel ek alanlar
&lt;/h2&gt;

&lt;p&gt;Benim tercih ettiğim yapı, tüm aktivite türleri için ortak bir çekirdek tablo ve türe özel ayrıntılar için yapılandırılmış bir ek alandır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="kt"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt;            &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'call'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'message'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'meeting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'task'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'note'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'site_visit'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;direction&lt;/span&gt;       &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'inbound'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'outbound'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'internal'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;          &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'planned'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'missed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;owner_id&lt;/span&gt;        &lt;span class="kt"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nf"&gt;app_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;occurred_at&lt;/span&gt;     &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- gerçekleştiği an (done/missed için)&lt;/span&gt;
  &lt;span class="n"&gt;due_at&lt;/span&gt;          &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- planlandığı an (planned için)&lt;/span&gt;
  &lt;span class="n"&gt;duration_sec&lt;/span&gt;    &lt;span class="kt"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;outcome&lt;/span&gt;         &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- 'reached','no_answer','busy','interested','not_interested'...&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;          &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 'manual','pbx','whatsapp','calendar','email_sync','automation'&lt;/span&gt;
  &lt;span class="n"&gt;source_ref&lt;/span&gt;      &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- dış sistemdeki kimlik&lt;/span&gt;
  &lt;span class="n"&gt;details&lt;/span&gt;         &lt;span class="kt"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;      &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;activity_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;activity_id&lt;/span&gt;  &lt;span class="kt"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nf"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;       &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'lead'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'deal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'company'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'unit'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;    &lt;span class="kt"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;is_primary&lt;/span&gt;   &lt;span class="kt"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;activity_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu şemada üç karar bilinçli.&lt;/p&gt;

&lt;p&gt;İlki, &lt;code&gt;occurred_at&lt;/code&gt; ile &lt;code&gt;created_at&lt;/code&gt; ayrımı. Bir danışman akşam saatinde, öğleden sonra yaptığı üç görüşmeyi CRM'e girebilir. Zaman çizelgesi ve raporlar &lt;code&gt;occurred_at&lt;/code&gt;'e göre, denetim ise &lt;code&gt;created_at&lt;/code&gt;'e göre çalışmalıdır. İkisini tek alanda birleştirirseniz ya sıralama bozulur ya da "kayıt ne zaman girildi?" bilgisi kaybolur.&lt;/p&gt;

&lt;p&gt;İkincisi, ilişkinin ayrı bir &lt;code&gt;activity_link&lt;/code&gt; tablosunda tutulması. Gayrimenkulde bir site ziyareti aynı anda bir kişiye, o kişinin fırsatına ve gezilen daireye (unit) bağlıdır. Tek bir &lt;code&gt;lead_id&lt;/code&gt; kolonu bu gerçeği taşıyamaz. Çoklu bağlantı sayesinde aynı aktivite hem kişinin hem fırsatın zaman çizelgesinde görünür; &lt;code&gt;is_primary&lt;/code&gt; ise raporlamada aktivitenin hangi kayda "sayılacağını" belirler.&lt;/p&gt;

&lt;p&gt;Üçüncüsü, &lt;code&gt;details&lt;/code&gt; alanının JSONB olması ama serbest bırakılmaması. Arama için kayıt dosyası kimliği ve santral dahilisi, mesaj için şablon adı ve teslim durumu, toplantı için katılımcılar gibi alanlar türe göre değişir. Bunları ana tabloya kolon olarak eklemek tabloyu şişirir; tamamen serbest bırakmak ise her geliştiricinin farklı anahtar kullanmasına yol açar. Uygulama katmanında tür başına bir şema (JSON Schema veya Pydantic modeli) tanımlayıp yazmadan önce doğrulamak, iki dünyanın iyi tarafını birleştirir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planlanmış ve gerçekleşmiş aktivite: aynı kayıt, iki hal
&lt;/h2&gt;

&lt;p&gt;Satış ekiplerinin en sık karıştırdığı şey, görevle yapılan işin farkıdır. "Yarın 14:00'te ara" bir plandır; ertesi gün yapılan arama ise bir olaydır. Bunları iki ayrı tabloda tutmak cazip görünür ama planın gerçekleşmeye dönüştüğü anı izlemeyi zorlaştırır.&lt;/p&gt;

&lt;p&gt;Ben aynı kaydın durum değiştirmesini tercih ediyorum: &lt;code&gt;planned&lt;/code&gt; olarak oluşturulan aktivite, yapıldığında &lt;code&gt;done&lt;/code&gt;, ulaşılamadığında &lt;code&gt;missed&lt;/code&gt;, vazgeçildiğinde &lt;code&gt;cancelled&lt;/code&gt; olur. Geçişleri uygulama katmanında açıkça tanımlamak gerekir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="no"&gt;ALLOWED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"planned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"missed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cancelled"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"missed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# geç de olsa ulaşıldıysa&lt;/span&gt;
    &lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="kt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# gerçekleşmiş olay değiştirilmez&lt;/span&gt;
    &lt;span class="s2"&gt;"cancelled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;ALLOWED&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"missed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"gerçekleşen aktivite için occurred_at zorunlu"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;
    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;occurred_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt;
    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;
    &lt;span class="nf"&gt;record_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;done&lt;/code&gt; durumundaki bir kaydın değiştirilememesi önemli bir kural. Gerçekleşmiş bir aramanın sonucu yanlış girildiyse, kaydı sessizce düzeltmek yerine bir düzeltme notu veya denetim satırı eklenmelidir. Aksi halde "bu fırsat neden kaybedildi?" sorusunu sorduğunuzda geçmiş, olduğu gibi değil, sonradan düzenlendiği haliyle karşınıza çıkar.&lt;/p&gt;

&lt;p&gt;Gecikmiş görevler de bu modelde doğal olarak hesaplanır: &lt;code&gt;status = 'planned' AND due_at &amp;lt; now()&lt;/code&gt; olan her kayıt gecikmiştir. Ayrı bir "overdue" bayrağı tutmaya gerek yoktur; tutarsanız mutlaka bir gün gerçeklikten kopar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dış kaynaklardan gelen aktiviteler: tekilleştirme ve eşleme
&lt;/h2&gt;

&lt;p&gt;Aktivitelerin çoğu artık elle girilmez. Bulut santral her aramayı webhook ile bildirir, WhatsApp Cloud API mesaj durumlarını gönderir, takvim entegrasyonu toplantıları senkronlar. Bu kaynakların ortak özelliği, aynı olayı birden fazla kez gönderebilmeleridir: yeniden deneme, durum güncellemesi veya sıra dışı teslim.&lt;/p&gt;

&lt;p&gt;Şemadaki &lt;code&gt;UNIQUE (source, source_ref)&lt;/code&gt; kısıtı bu yüzden var. Her kaynak kendi olay kimliğini taşır ve kayıt "ekle ya da güncelle" mantığıyla yazılır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration_sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'call'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'outbound'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pbx'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duration_sec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration_sec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;outcome&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;details&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Asıl zor kısım tekilleştirme değil, eşlemedir: santralden gelen bir arama hangi lead'e aittir? Elinizde genelde yalnızca bir telefon numarası vardır. Burada daha önce lead deduplication için kullandığımız normalizasyonun aynısını kullanmak şart. Santral numarayı &lt;code&gt;05xx...&lt;/code&gt; biçiminde, form &lt;code&gt;+90 5xx...&lt;/code&gt; biçiminde, WhatsApp ise &lt;code&gt;905xx...&lt;/code&gt; biçiminde gönderebilir. Hepsini E.164 biçimine çevirip normalize edilmiş numara üzerinden arama yapmazsanız, aynı müşterinin aramaları üç farklı "bilinmeyen numara" kaydına dağılır.&lt;/p&gt;

&lt;p&gt;Eşleme belirsiz olduğunda (aynı numara iki farklı kişide kayıtlıysa veya hiç eşleşme yoksa) aktiviteyi atmamak, &lt;code&gt;activity_link&lt;/code&gt; olmadan bir "eşleşmemiş aktiviteler" kuyruğunda tutmak gerekir. Bu kuyruk hem veri kalitesi sorunlarını görünür kılar hem de yeni bir lead'in ilk temasının kaybolmasını önler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Türetilmiş alanlar: son temas, sıradaki adım, ilk dokunuş
&lt;/h2&gt;

&lt;p&gt;Zaman çizelgesi her açıldığında tüm aktiviteleri taramak küçük veriyle sorun değildir, ama liste ekranlarında yüzlerce lead için "son temas" ve "sıradaki adım" göstermek gerekir. Bu yüzden türetilmiş alanları bir özet tablosunda tutmak mantıklıdır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;entity_activity_summary&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;             &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;          &lt;span class="kt"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;first_touch_at&lt;/span&gt;     &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_touch_at&lt;/span&gt;      &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_touch_type&lt;/span&gt;    &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;next_due_at&lt;/span&gt;        &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;next_activity_id&lt;/span&gt;   &lt;span class="kt"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;done_count&lt;/span&gt;         &lt;span class="kt"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kritik nokta, "temas"ın tanımını tek bir yerde yazmaktır. Bir not eklemek temas mıdır? Teslim edilmemiş bir WhatsApp mesajı temas mıdır? Cevapsız bir arama? Benim kullandığım tanım şu: müşteriye yönelik, gerçekleşmiş (&lt;code&gt;done&lt;/code&gt;) ve &lt;code&gt;direction&lt;/code&gt; değeri &lt;code&gt;inbound&lt;/code&gt; ya da &lt;code&gt;outbound&lt;/code&gt; olan aktiviteler temastır; iç notlar ve iptal edilmiş görevler değildir. Cevapsız arama "deneme" olarak ayrıca sayılır ama son temas tarihini güncellemez. Bu tanımı bir SQL görünümü veya tek bir fonksiyon olarak yazar, özet tablosunu yalnızca oradan güncellersiniz.&lt;/p&gt;

&lt;p&gt;Özet tablosunu tetikleyiciyle (trigger) güncellemek mümkün ama dış kaynaklardan yoğun olay geldiğinde kilitlenme yaratabilir. Ben genelde aktivite yazıldığında etkilenen kayıtları bir kuyruğa atıp kısa aralıklarla yeniden hesaplayan bir iş kullanıyorum. Yeniden hesaplama her zaman ham aktivite tablosundan yapıldığı için özet tablo bozulsa bile silip baştan üretilebilir. Bu, özet tablonun bir "gerçek kaynağı" değil, bir önbellek olduğunu hatırlatan iyi bir disiplin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Otomasyonların ürettiği aktiviteler
&lt;/h2&gt;

&lt;p&gt;Otomatik karşılama mesajı, hatırlatma e-postası veya randevu teyidi gibi sistem tarafından yapılan işler de aktivitedir ve zaman çizelgesinde görünmelidir. Aksi halde danışman, müşterinin zaten üç mesaj aldığını bilmeden dördüncüyü gönderir.&lt;/p&gt;

&lt;p&gt;Ancak otomasyon aktivitelerini insan aktiviteleriyle aynı şekilde saymak raporları yanıltır. "Danışman başına günlük temas" metriğine otomatik mesajlar girerse, en çok otomasyonu tetikleyen danışman en çalışkan görünür. Bu yüzden &lt;code&gt;source = 'automation'&lt;/code&gt; olan kayıtları ve &lt;code&gt;owner_id&lt;/code&gt; değeri boş olan kayıtları performans raporlarından ayırmak, ama müşteri zaman çizelgesinde göstermek gerekir. İlk dokunuş süresi gibi ölçümlerde de hangi tanımı kullandığınızı açıkça belirtin: otomatik karşılama mesajı ilk dokunuş sayılıyorsa bu süre her zaman çok kısa çıkar ve gerçek insan yanıt süresini gizler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raporlama: aktivite verisinden anlamlı soru üretmek
&lt;/h2&gt;

&lt;p&gt;İyi modellenmiş aktivite verisi, satış yönetiminin sorduğu birçok soruya doğrudan cevap verir. Örneğin her lead için ilk insan temasına kadar geçen süreyi şöyle hesaplayabilirsiniz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_human_touch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;time_to_first_touch&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nb"&gt;lead&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;activity_link&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'lead'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'outbound'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'automation'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aynı yapı üzerinden aşamalar arası aktivite yoğunluğu (bir fırsat kapanana kadar kaç temas gerekti), danışman başına gecikmiş görev sayısı veya kanal bazında ulaşılabilirlik oranı da hesaplanabilir. Burada rakamın kendisinden önce tanımın tutarlılığı önemlidir. Aynı metriği iki farklı ekranda iki farklı sorguyla hesaplarsanız, toplantıda rakamları değil sorguları tartışırsınız. Metrik tanımlarını görünüm olarak veritabanında tutmak bu tartışmayı baştan bitirir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kişisel veri ve saklama
&lt;/h2&gt;

&lt;p&gt;Aktivite tablosu bir CRM'deki en yoğun kişisel veri kaynaklarından biridir: arama kayıtları, mesaj içerikleri, serbest metin notlar. Bu yüzden saklama ve maskeleme kurallarının aktivite tablosunu da kapsaması gerekir. &lt;code&gt;details&lt;/code&gt; alanındaki mesaj gövdesi veya kayıt dosyası bağlantısı, lead anonimleştirildiğinde ayrıca temizlenmelidir; aktivitenin türü, tarihi, süresi ve sonucu ise istatistik için kalabilir. Hangi alanın ne kadar tutulacağı hukuk ekibinizin kararıdır, bu yazı hukuki görüş değildir; ama şemayı baştan bu ayrıma uygun kurmak, o karar geldiğinde uygulamayı kolaylaştırır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık yapılan hatalar
&lt;/h2&gt;

&lt;p&gt;Sahada en sık gördüğüm hataları kısaca toplarsam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Her kanal için ayrı tablo açmak ve zaman çizelgesini uygulama katmanında birleştirmeye çalışmak.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;occurred_at&lt;/code&gt; yerine &lt;code&gt;created_at&lt;/code&gt; ile sıralamak; sonradan girilen kayıtların çizelgeyi karıştırması.&lt;/li&gt;
&lt;li&gt;Gerçekleşmiş aktiviteleri sessizce düzenlemeye izin vermek.&lt;/li&gt;
&lt;li&gt;Telefon numarasını normalize etmeden eşleme yapmak.&lt;/li&gt;
&lt;li&gt;Otomasyon aktivitelerini insan performansına saymak.&lt;/li&gt;
&lt;li&gt;"Son temas" tanımını her raporda farklı yazmak.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SSS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CRM'de activity ile task arasındaki fark nedir?&lt;/strong&gt;&lt;br&gt;
Task, planlanmış bir aktivitedir. Aynı kayıt &lt;code&gt;planned&lt;/code&gt; olarak oluşturulur ve yapıldığında &lt;code&gt;done&lt;/code&gt;, ulaşılamadığında &lt;code&gt;missed&lt;/code&gt; olur. İkisini aynı tabloda tutmak planla gerçekleşme arasındaki geçişi izlemeyi kolaylaştırır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Her kanal için ayrı tablo tutmak hiç mantıklı değil mi?&lt;/strong&gt;&lt;br&gt;
Kanala özel büyük veriler (örneğin mesaj gövdeleri veya kayıt dosyası meta verisi) ayrı tablolarda durabilir. Ama zaman çizelgesi, sıralama ve raporlama için ortak bir çekirdek aktivite kaydı olmalı; kanal tabloları bu kayda bağlanmalı.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aynı arama santralden iki kez gelirse ne olur?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;source&lt;/code&gt; ve &lt;code&gt;source_ref&lt;/code&gt; üzerindeki tekil kısıt sayesinde ikinci olay yeni kayıt açmaz, mevcut kaydı günceller. Güncellemenin hangi alanlara izin verdiğini açıkça yazmak gerekir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eşleşmeyen aktiviteler ne yapılmalı?&lt;/strong&gt;&lt;br&gt;
Atılmamalı. Bağlantısız olarak saklanıp bir kuyrukta gösterilmeli; yeni bir müşterinin ilk teması çoğu zaman tam da böyle bir kayıttır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Son temas tarihini tetikleyiciyle mi güncellemeliyim?&lt;/strong&gt;&lt;br&gt;
Düşük hacimde tetikleyici yeterlidir. Dış kaynaklardan yoğun olay geliyorsa, etkilenen kayıtları kuyruğa atıp ham tablodan yeniden hesaplayan bir iş daha güvenlidir ve özet tablo bozulduğunda baştan üretilebilir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Otomatik mesajlar zaman çizelgesinde görünmeli mi?&lt;/strong&gt;&lt;br&gt;
Evet, müşterinin ne aldığını danışmanın görmesi gerekir. Ancak performans ve ilk yanıt süresi gibi metriklerde insan aktivitelerinden ayrı tutulmalıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Son söz
&lt;/h2&gt;

&lt;p&gt;Aktivite modeli, bir CRM'in "ne oldu?" sorusuna verdiği cevaptır. Lead, fırsat ve aşama tabloları bir kaydın şu anki halini anlatır; aktivite tablosu ise oraya nasıl gelindiğini. Bu tabloyu ortak bir çekirdekle, açık durum geçişleriyle, kaynak bazında tekilleştirmeyle ve tek yerde tanımlanmış türetilmiş alanlarla kurduğunuzda, satış ekibinin güvendiği bir zaman çizelgesi ve yönetimin tartışmadan kullanabildiği raporlar elde edersiniz. Benim lead yönetimi ve CRM projelerinde gördüğüm, sorunların büyük kısmının bu tabloyu baştan doğru kurmamaktan kaynaklandığı.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>backend</category>
      <category>crm</category>
    </item>
    <item>
      <title>CRM'de data retention'ı koda dökmek: Kural tablosu, güvenli saklama işi, legal hold ve kripto-parçalama</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Tue, 10 Nov 2026 08:21:39 +0000</pubDate>
      <link>https://dev.to/korayyalcin/crmde-data-retentioni-koda-dokmek-kural-tablosu-guvenli-saklama-isi-legal-hold-ve-oei</link>
      <guid>https://dev.to/korayyalcin/crmde-data-retentioni-koda-dokmek-kural-tablosu-guvenli-saklama-isi-legal-hold-ve-oei</guid>
      <description>&lt;p&gt;Bir CRM'de "veriyi ne kadar saklıyoruz?" sorusu genelde bir politika belgesiyle cevaplanır ve orada kalır. Belgede "pasif lead kayıtları iki yıl sonra anonimleştirilir" yazar, ama veritabanında bu cümleyi uygulayan tek satır kod yoktur. Üç yıl sonra biri yedekten eski bir tabloyu geri yüklediğinde, silindiği sanılan telefon numaraları da geri gelir.&lt;/p&gt;

&lt;p&gt;Ben Koray Yalçın; yıllardır lead yönetimi, CRM kurulumu ve gayrimenkul satış operasyonları üzerine çalışıyorum. Bu yazıda data retention'ı, yani veri saklama süresini, bir geliştiricinin gözünden ele alıyorum: politikayı koda nasıl çevirirsiniz, saklama işini nasıl güvenli çalıştırırsınız, yasal tutma (legal hold) istisnasını nasıl modellersiniz, yedeklerdeki kopyalarla ne yaparsınız ve tüm bunların gerçekten çalıştığını nasıl kanıtlarsınız. Politikanın içeriği, yani hangi verinin kaç yıl tutulacağı, hukuk ekibinizin işidir; bu yazı hukuki görüş değildir. Benim derdim, karar verildikten sonra o kararın sistemde gerçekten uygulanması.&lt;/p&gt;

&lt;h2&gt;
  
  
  Politika belgesi neden yetmez?
&lt;/h2&gt;

&lt;p&gt;Saklama politikalarının sahada çökmesinin üç tipik nedeni var.&lt;/p&gt;

&lt;p&gt;Birincisi, kural insan hafızasına bırakılır. "Ayda bir eski kayıtları temizleriz" diye başlanır, iki ay sonra unutulur. İkincisi, kural tek bir tabloya uygulanır ama veri on yere kopyalanmıştır: ana &lt;code&gt;leads&lt;/code&gt; tablosu temizlenir, fakat arama kayıtları, WhatsApp mesaj geçmişi, dışa aktarılmış CSV'ler, analitik ambarı ve hata loglarında aynı kişinin bilgisi durmaya devam eder. Üçüncüsü, silme işi bir kez yanlış çalışır ve fazla veri siler; ekip korkup işi tamamen kapatır.&lt;/p&gt;

&lt;p&gt;Bu üç sorunun çözümü aynı yere çıkıyor: saklama kuralını veri olarak tutmak, tek bir iş tarafından uygulamak ve her çalıştırmayı kayda geçirmek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kuralı kod değil veri olarak tutun
&lt;/h2&gt;

&lt;p&gt;Saklama süresini &lt;code&gt;if&lt;/code&gt; bloklarına gömmek yerine bir tabloda tutmak, hem değişikliği kolaylaştırır hem de denetimde "kural neydi?" sorusuna net cevap verir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;retention_rule&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="kt"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;          &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 'lead', 'call_recording', 'message', 'export_file'&lt;/span&gt;
  &lt;span class="n"&gt;condition_key&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 'stage=lost', 'stage=won', 'no_activity'&lt;/span&gt;
  &lt;span class="n"&gt;anchor_field&lt;/span&gt;    &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- süre hangi tarihten sayılır: 'last_activity_at', 'closed_at'&lt;/span&gt;
  &lt;span class="n"&gt;keep_days&lt;/span&gt;       &lt;span class="kt"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;          &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anonymize'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'delete'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'archive'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;version&lt;/span&gt;         &lt;span class="kt"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;approved_by&lt;/span&gt;     &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;effective_from&lt;/span&gt;  &lt;span class="kt"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;condition_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada iki ayrıntı önemli. İlki &lt;code&gt;anchor_field&lt;/code&gt;: sürenin neye göre sayıldığı. Kaybedilmiş bir lead için çoğu zaman oluşturulma tarihi değil son temas tarihi anlamlıdır; satılmış bir konutun müşteri kaydı için ise satışın kapandığı tarih. Bu çapayı açıkça yazmazsanız, her geliştirici farklı bir tarih kullanır. İkincisi &lt;code&gt;version&lt;/code&gt;: kural değiştiğinde eski satırı güncellemezsiniz, yeni sürüm eklersiniz. Böylece bir kaydın neden o tarihte anonimleştirildiğini geriye dönük açıklayabilirsiniz.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;action&lt;/code&gt; alanındaki üç seçenek de bilinçli bir ayrım. Gayrimenkul CRM'inde satılmış dairenin sözleşme bağlantılı müşteri kaydını silemezsiniz; ama ilgilenip almamış bir adayın telefonunu iki yıl sonra tutmanın çoğu zaman bir gerekçesi kalmaz. Anonimleştirme, kişiyi tanımlayan alanları kaldırıp istatistik için gerekli olanları (kanal, proje, aşama, tarih) bırakır. Böylece "geçen yıl hangi kampanyadan kaç lead geldi?" raporu bozulmaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hangi alan kişisel, hangisi değil: alan sınıflandırması
&lt;/h2&gt;

&lt;p&gt;Anonimleştirmenin doğru çalışması, hangi kolonun kişisel veri olduğunu bilmeye bağlı. Bunu da yine bir kayıt defterinde tutmanızı öneririm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;field_classification&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;      &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;       &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt;    &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'identifier'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'free_text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'business'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'technical'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;on_anonymize&lt;/span&gt; &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_anonymize&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'null'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'redact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'keep'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;free_text&lt;/code&gt; kategorisini özellikle vurgulamak istiyorum. Satış danışmanlarının not alanına "Ahmet Bey'in eşi Ayşe Hanım 0533... numarasından aradı" gibi cümleler yazdığını hepimiz biliyoruz. Ad ve telefon kolonlarını boşaltıp not alanını bırakırsanız, anonimleştirme kâğıt üzerinde kalır. Serbest metin alanlarında ya tamamını kaldırmak ya da desen tabanlı bir temizleme (telefon, e-posta, TC kimlik deseni) uygulamak gerekir; ben genelde tamamen kaldırmayı ve notun yalnızca uzunluğunu ve tarihini bırakmayı tercih ediyorum.&lt;/p&gt;

&lt;p&gt;Yeni bir kolon eklendiğinde bu tabloda karşılığı yoksa CI'ın hata vermesi de iyi bir alışkanlık. Basit bir test, şemadaki her kolonun sınıflandırıldığını kontrol edebilir. Böylece altı ay sonra eklenen &lt;code&gt;secondary_phone&lt;/code&gt; alanı sessizce saklama kuralının dışında kalmaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saklama işini güvenli çalıştırmak
&lt;/h2&gt;

&lt;p&gt;Kural ve sınıflandırma hazır olduğunda geriye bunları uygulayan iş kalır. Burada hız değil güvenlik önceliklidir. Benim kullandığım iskelet şöyle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_retention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;start_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keep_days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;under_legal_hold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;log_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;apply_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# anonymize / delete / archive&lt;/span&gt;
                &lt;span class="nf"&gt;write_tombstones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;enqueue_downstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# outbox: diğer sistemlere yay&lt;/span&gt;
        &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;finish_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu iskeletteki her parça bir hatadan ders alınarak eklenmiş durumda.&lt;/p&gt;

&lt;p&gt;Kuru çalıştırma (dry run) varsayılan olmalı. Yeni bir kural ya da yeni bir sürüm devreye girdiğinde iş önce sadece "şu kayıtlar etkilenecekti" listesini üretir. Bu listeyi bir insan gözden geçirdikten sonra gerçek mod açılır.&lt;/p&gt;

&lt;p&gt;Toplu işleme ve üst sınır (&lt;code&gt;max_total&lt;/code&gt;) şart. Bir kuralda &lt;code&gt;keep_days&lt;/code&gt; yanlışlıkla 730 yerine 73 girildiğinde, üst sınır olmayan bir iş bütün tabloyu bir gecede anonimleştirebilir. Üst sınıra takılan bir çalıştırma alarm üretmeli ve otomatik devam etmemeli.&lt;/p&gt;

&lt;p&gt;Mezar taşı (tombstone) kaydı, silinen veya anonimleştirilen kaydın kimliğini, hangi kural sürümüyle ve hangi çalıştırmada işlendiğini tutar; kişisel veri içermez. Bu kayıt iki işe yarar: denetimde kanıt olur ve yedekten geri yükleme sonrası temizliğin tekrar uygulanmasını sağlar.&lt;/p&gt;

&lt;p&gt;Aşağı akış kuyruğu (outbox) ise asıl unutulan kısım. Ana veritabanında anonimleştirdiğiniz kişi, reklam kitlelerinde, e-posta aracında, analitik ambarında ve WhatsApp sağlayıcısında yaşamaya devam ediyorsa iş yarım kalmıştır. Aynı işlem içinde outbox'a yazılan olaylar, her hedef sistem için ayrı bir tüketici tarafından işlenir ve başarısız olanlar yeniden denenir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yasal tutma (legal hold) istisnası
&lt;/h2&gt;

&lt;p&gt;Bazen saklama süresi dolmuş bir kaydı silmemeniz gerekir: devam eden bir uyuşmazlık, bir şikâyet incelemesi, bir resmi talep. Bu durumu her geliştiricinin kendi koduna &lt;code&gt;if customer_id == 4812&lt;/code&gt; diye yazması felakettir. Ayrı bir tablo kullanın:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;legal_hold&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="kt"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;      &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;   &lt;span class="kt"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt;      &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opened_by&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opened_at&lt;/span&gt;   &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;review_at&lt;/span&gt;   &lt;span class="kt"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;closed_at&lt;/span&gt;   &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;legal_hold&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;closed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;review_at&lt;/code&gt; alanı zorunlu olmalı. Açılıp unutulan tutmalar, saklama politikasının sessizce delinmesinin en yaygın yoludur. Gözden geçirme tarihi geçmiş açık tutmalar için haftalık bir rapor üretin. Tutma kapandığında kayıt bir sonraki saklama çalıştırmasında normal kurala döner; ayrıca bir işlem yapmanız gerekmez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yedekler ve kopyalar: kripto-parçalama (crypto-shredding)
&lt;/h2&gt;

&lt;p&gt;En zor soru yedeklerdir. Günlük yedekleri otuz gün, aylık yedekleri bir yıl tutuyorsanız, bugün anonimleştirdiğiniz kişi yedeklerin içinde bir yıl daha durur. Yedekleri tek tek açıp düzenlemek pratik değildir.&lt;/p&gt;

&lt;p&gt;İki yaklaşım var ve çoğu zaman ikisini birlikte kullanıyorum.&lt;/p&gt;

&lt;p&gt;Birincisi, geri yükleme sonrası yeniden uygulama. Yedekten geri dönüldüğünde, mezar taşı tablosu (ki ayrı ve daha uzun süre saklanır) okunur ve oradaki tüm kayıtlara anonimleştirme tekrar uygulanır. Bunu geri yükleme prosedürünün zorunlu bir adımı yapın; elle hatırlanmasını beklemeyin.&lt;/p&gt;

&lt;p&gt;İkincisi, kişi başına anahtarla şifreleme. Telefon, e-posta ve ad gibi alanlar kişiye özel bir veri anahtarıyla şifrelenir; anahtarlar ayrı bir anahtar deposunda durur. Saklama süresi dolduğunda anahtarı silersiniz. Yedeklerde şifreli veri kalsa bile artık okunamaz.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shred_subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# geri dönüşsüz&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"UPDATE subject SET key_ref = NULL, shredded_at = now() WHERE id = %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"subject_shredded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key_ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kripto-parçalamanın bir bedeli var: şifreli alanlarda arama ve tekilleştirme zorlaşır. Bu yüzden arama için normalleştirilmiş değerin karma (hash) hâlini ayrı bir kolonda tutmak ve onu da saklama kuralına dahil etmek gerekir. Küçük bir CRM için bu mimari ağır gelebilir; o durumda kısa yedek süresi ve geri yükleme sonrası yeniden uygulama çoğu ihtiyacı karşılar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unutulan kopyalar listesi
&lt;/h2&gt;

&lt;p&gt;Saklama işinin kapsamını belirlerken her ekipte şu listeyi tek tek kontrol ediyorum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dışa aktarılmış CSV ve Excel dosyaları (ortak sürücüler, e-posta ekleri).&lt;/li&gt;
&lt;li&gt;Uygulama ve hata logları; özellikle istek gövdesini loglayan ara katmanlar.&lt;/li&gt;
&lt;li&gt;Arama kayıtları ve çağrı merkezi transkriptleri.&lt;/li&gt;
&lt;li&gt;WhatsApp ve SMS sağlayıcılarındaki mesaj geçmişi.&lt;/li&gt;
&lt;li&gt;Analitik ambarı ve oradan türetilen tablolar.&lt;/li&gt;
&lt;li&gt;Reklam platformlarına yüklenmiş özel kitleler.&lt;/li&gt;
&lt;li&gt;Test ve geliştirme ortamlarına kopyalanmış üretim verisi.&lt;/li&gt;
&lt;li&gt;Kuyruklardaki ölü mektup (DLQ) mesajları.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bu listedeki her kalem için "sahibi kim, saklama süresi ne, silme nasıl tetikleniyor" sorularının cevabı yazılı olmalı. Cevabı olmayan kalem, politikanın deliğidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gerçekten çalıştığını kanıtlamak
&lt;/h2&gt;

&lt;p&gt;Saklama işi sessiz çalışan bir iştir; çalışmadığını genelde bir denetimde ya da bir şikâyette öğrenirsiniz. Bunu önlemek için üç kontrol kuruyorum.&lt;/p&gt;

&lt;p&gt;İlki, çalıştırma kaydı ve sapma alarmı. Her çalıştırma &lt;code&gt;retention_run&lt;/code&gt; tablosuna başlangıç, bitiş, işlenen kayıt sayısı ve üst sınıra takılıp takılmadığı bilgisiyle yazılır. Bir kural için beklenen hacim birden sıfıra düşerse ya da birden on katına çıkarsa alarm üretilir.&lt;/p&gt;

&lt;p&gt;İkincisi, bağımsız doğrulama sorgusu. Saklama işinden ayrı yazılmış basit bir sorgu, "süresi dolmuş, tutmada olmayan ama hâlâ kişisel verisi duran kayıt var mı?" diye bakar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="cm"&gt;(*)
FROM lead l
JOIN retention_rule r ON r.entity = 'lead' AND r.condition_key = 'stage=lost'
WHERE l.stage = 'lost'
  AND l.last_activity_at &amp;lt; now() - (r.keep_days || ' days')::interval
  AND l.phone IS NOT NULL
  AND NOT EXISTS (SELECT 1 FROM legal_hold h
                  WHERE h.entity = 'lead' AND h.entity_id = l.id AND h.closed_at IS NULL);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu sayı sıfır değilse iş ya çalışmıyor ya da bir koşulu kaçırıyor demektir.&lt;/p&gt;

&lt;p&gt;Üçüncüsü, aylık örneklem. Rastgele birkaç anonimleştirilmiş kaydı açıp serbest metin alanlarında, ilişkili tablolarda ve aşağı akış sistemlerinde iz kalıp kalmadığına bakmak. Otomasyonun yakalayamadığı şeyleri genelde bu elle kontrol gösterir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gayrimenkul CRM'inden bir senaryo
&lt;/h2&gt;

&lt;p&gt;Kıbrıs'ta konut projesi satan bir firmayı düşünün. Lansman döneminde Meta ve Google reklamlarından binlerce lead gelir; bunların küçük bir kısmı satışa döner. Kaybedilen adaylar için kural "son temastan 730 gün sonra anonimleştir" olsun, satılmış müşteriler için ise sözleşme kaydı ayrı tutulur ve anonimleştirilmez.&lt;/p&gt;

&lt;p&gt;Bir aday, satıştan vazgeçtikten sonra şikâyet başvurusu yapmış olsun. Hukuk ekibi bu kayıt için yasal tutma açar, gözden geçirme tarihini altı ay sonrasına koyar. Saklama işi her gece çalışır, bu adayı atlar; diğer süresi dolmuş adayları anonimleştirir, mezar taşı yazar ve outbox üzerinden reklam kitlelerinden ve WhatsApp sağlayıcısından çıkarma olaylarını gönderir. Kampanya raporları bozulmaz, çünkü kanal, proje ve aşama bilgisi duruyor. Altı ay sonra tutma kapanınca kayıt bir sonraki çalıştırmada normal kurala döner.&lt;/p&gt;

&lt;p&gt;Bu senaryodaki hiçbir adım bir insanın bir şeyi hatırlamasına bağlı değil. Saklama politikasının sahada yaşamasının tek yolu bu.&lt;/p&gt;

&lt;p&gt;Saklama süresinin kavramsal tanımı ve KVKK bağlamı için sitemdeki &lt;a href="https://www.korayyalcin.org/yayinlar-arastirmalar/sozluk/data-retention/" rel="noopener noreferrer"&gt;data retention nedir&lt;/a&gt; sözlük maddesine bakabilirsiniz; bu yazı onun uygulama tarafını anlatıyor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık Sorulan Sorular
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Silmek mi, anonimleştirmek mi daha doğru?&lt;/strong&gt;&lt;br&gt;
Kişisel veriye artık ihtiyaç yoksa ama kaydın istatistiksel değeri varsa anonimleştirme çoğu zaman daha iyi bir denge sunar. Hangisinin hukuken yeterli olduğu ise sizin politikanıza ve hukuk ekibinizin değerlendirmesine bağlıdır. Teknik olarak önemli olan, anonimleştirmenin gerçekten geri döndürülemez olması: karma değerler tuzsuz ve tahmin edilebilir girdilerden üretiliyorsa, telefon numarası gibi alanlar kolayca geri bulunabilir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saklama işi ne sıklıkla çalışmalı?&lt;/strong&gt;&lt;br&gt;
Günlük çalıştırma çoğu CRM için yeterli. Önemli olan sıklıktan çok her çalıştırmanın kayda geçmesi ve bir gece çalışmadığında bunun fark edilmesi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kural değiştiğinde geçmiş kayıtlar ne olacak?&lt;/strong&gt;&lt;br&gt;
Yeni sürüm eklendiğinde ilk çalıştırmayı kuru modda yapın. Süre kısalıyorsa birden çok kaydı etkileyebilir; üst sınır ve gözden geçirme bu noktada sizi korur. Süre uzuyorsa, zaten anonimleştirilmiş kayıtlar geri gelmez; bunu ekibe açıkça söyleyin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yasal tutmadaki bir kayıt diğer sistemlerde de korunmalı mı?&lt;/strong&gt;&lt;br&gt;
Evet. Tutma bilgisi yalnızca ana veritabanında kalırsa, aşağı akış sistemlerindeki kendi saklama kuralları veriyi silebilir. Tutma açıldığında outbox üzerinden ilgili sistemlere de bildirim gitmesi gerekir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Küçük bir ekip için en az ne yapılmalı?&lt;/strong&gt;&lt;br&gt;
Kural tablosu, alan sınıflandırması, kuru modla başlayan günlük bir iş, mezar taşı kaydı ve bağımsız doğrulama sorgusu. Kripto-parçalama ve gelişmiş aşağı akış yayılımı sonraki adım olabilir; ama dışa aktarılmış dosyalar ve loglar listesi ilk günden kapsama girmeli.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Koray Yalçın; lead yönetimi, CRM mimarisi, dijital pazarlama ve gayrimenkul teknolojileri (PropTech) üzerine çalışıyor ve yazıyor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>privacy</category>
      <category>backend</category>
      <category>crm</category>
    </item>
    <item>
      <title>fbclid'i formdan Meta Conversions API'ye taşımak: _fbc, hash'leme, event_id ve CRM aşama eşlemesi</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Mon, 09 Nov 2026 16:28:12 +0000</pubDate>
      <link>https://dev.to/korayyalcin/fbclidi-formdan-meta-conversions-apiye-tasimak-fbc-hashleme-eventid-ve-crm-asama-eslemesi-1dma</link>
      <guid>https://dev.to/korayyalcin/fbclidi-formdan-meta-conversions-apiye-tasimak-fbc-hashleme-eventid-ve-crm-asama-eslemesi-1dma</guid>
      <description>&lt;p&gt;Bir konut projesinin Instagram reklamına tıklayan kişi formu doldurur, satış danışmanı onu arar, hafta sonu örnek daireyi gezer ve belki bir buçuk ay sonra satış ofisinde sözleşme imzalar. Meta tarafında ise genellikle hikâye form gönderiminde biter: Pixel bir &lt;code&gt;Lead&lt;/code&gt; olayı görür, sonrasını hiç öğrenmez. Reklam algoritması da "form dolduran" kişiye benzeyenleri bulmaya devam eder. Lead yönetimi ve CRM kurulumlarında en sık gördüğüm kopukluk budur: reklam paneli hacmi optimize eder, satış ekibi ise kaliteden şikâyet eder.&lt;/p&gt;

&lt;p&gt;Bu yazıda o kopukluğu kapatan teknik hattı anlatacağım: tarayıcıda &lt;code&gt;fbclid&lt;/code&gt; değerini rızaya bağlı olarak yakalamak, ondan &lt;code&gt;_fbc&lt;/code&gt; değerini doğru biçimde üretmek, &lt;code&gt;_fbp&lt;/code&gt; ile birlikte sunucuda bir temas (touch) tablosunda saklamak, CRM aşamalarını Meta olaylarına eşlemek ve bu olayları Conversions API (CAPI) ile, Pixel'le çakışmadan, hash'lenmiş kullanıcı verisiyle göndermek. Kavramın tanımına değil, geliştiricinin önüne çıkacak kararlara odaklanacağım.&lt;/p&gt;

&lt;p&gt;Kısa bir bağlam: &lt;code&gt;fbclid&lt;/code&gt;, Meta (Facebook ve Instagram) reklamına tıklanıp harici bir siteye gidildiğinde hedef URL'ye eklenen tıklama kimliğidir. Tek başına bir anlam taşımaz; değeri, sonradan gönderilen sunucu olaylarının hangi reklam tıklamasına ait olduğunu Meta'nın anlayabilmesini sağlamasından gelir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixel neden yetmiyor, CAPI neyi değiştiriyor?
&lt;/h2&gt;

&lt;p&gt;Pixel tarayıcıda çalışır. Reklam engelleyiciler, tarayıcı gizlilik kısıtlamaları, çerez rızasının reddedilmesi ya da sayfanın olay tetiklenmeden kapanması gibi sebeplerle bazı olaylar hiç ulaşmaz. Ama gayrimenkul ve B2B satış gibi uzun döngülü işlerde asıl sorun bu kayıp bile değil: değerli olayların büyük kısmı zaten tarayıcıda olmuyor. "Danışman telefonda nitelikli buldu", "saha ziyareti yapıldı", "kapora alındı", "sözleşme imzalandı" gibi aşamalar CRM'de, satış ofisinde ya da telefonda yaşanır. Bunları Meta'ya bildirmenin tek yolu sunucudan göndermektir.&lt;/p&gt;

&lt;p&gt;Conversions API, işletmenin sunucusundan Meta'ya doğrudan olay göndermeyi sağlar. Burada iki ayrı fayda var ve bunları karıştırmamak gerekiyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yedeklilik:&lt;/strong&gt; Form gönderimi gibi tarayıcıda da yaşanan olayı hem Pixel hem CAPI ile göndermek. Burada amaç kaybı azaltmak; çift sayımı önlemek için tekilleştirme şart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derinlik:&lt;/strong&gt; Sadece CRM'de yaşanan olayları (nitelikli lead, satış) göndermek. Burada Pixel'in karşılığı yok; eşleştirme tamamen sizin gönderdiğiniz &lt;code&gt;fbc&lt;/code&gt;, &lt;code&gt;fbp&lt;/code&gt; ve hash'lenmiş kişisel verilerle yapılır.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;İkinci fayda, &lt;code&gt;fbclid&lt;/code&gt;'i formdan CRM'e kadar kaybetmeden taşımanın neden önemli olduğunu açıklıyor. Kırk beş gün sonra imzalanan sözleşmede elinizde sadece bir telefon numarası varsa eşleşme Meta'nın o numarayı tanımasına kalır. Elinizde tıklama anında üretilmiş bir &lt;code&gt;fbc&lt;/code&gt; değeri de varsa, olayı doğrudan o tıklamaya bağlamak için çok daha güçlü bir sinyal göndermiş olursunuz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yakalama: Rıza, TTL ve doğru _fbc biçimi
&lt;/h2&gt;

&lt;p&gt;Pixel kuruluysa ve rıza verilmişse, Pixel zaten &lt;code&gt;_fbc&lt;/code&gt; ve &lt;code&gt;_fbp&lt;/code&gt; adlı birinci taraf çerezleri yazar. &lt;code&gt;_fbp&lt;/code&gt; tarayıcı kimliğidir (&lt;code&gt;fb.1.&amp;lt;oluşturma zamanı ms&amp;gt;.&amp;lt;rastgele sayı&amp;gt;&lt;/code&gt; biçiminde), &lt;code&gt;_fbc&lt;/code&gt; ise tıklama kimliğini taşır. Yine de kendi küçük yakalama katmanımı yazmayı tercih ediyorum, çünkü form sayfası her zaman Pixel'in yüklendiği sayfa olmayabiliyor, bazı projelerde Pixel bilinçli olarak geç yükleniyor ve rıza durumuna göre davranışı kendim kontrol etmek istiyorum.&lt;/p&gt;

&lt;p&gt;Meta'nın belgelerine göre &lt;code&gt;fbc&lt;/code&gt; değerinin biçimi &lt;code&gt;version.subdomainIndex.creationTime.fbclid&lt;/code&gt; şeklindedir. &lt;code&gt;version&lt;/code&gt; her zaman &lt;code&gt;fb&lt;/code&gt;; &lt;code&gt;subdomainIndex&lt;/code&gt; çerezin tanımlandığı alan adı düzeyini belirtir ve değer sunucuda üretiliyorsa &lt;code&gt;1&lt;/code&gt; kullanılması öneriliyor; &lt;code&gt;creationTime&lt;/code&gt; milisaniye cinsinden Unix zamanıdır ve &lt;code&gt;fbclid&lt;/code&gt;'in ilk görüldüğü anı göstermelidir. Son parça da URL'deki &lt;code&gt;fbclid&lt;/code&gt; değerinin kendisidir. Burada sık yapılan iki hata var: zamanı saniye cinsinden yazmak ve &lt;code&gt;fbclid&lt;/code&gt; değerini küçük harfe çevirmek ya da kırpmak. Değer büyük-küçük harfe duyarlıdır, olduğu gibi taşınmalıdır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fbclid yakalama: rızaya bağlı, TTL'li, _fbc biçiminde&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FBC_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kc_meta_click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TTL_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// kendi saklama politikanıza göre belirleyin&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(?:^|; )&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=([^;]*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;captureMetaClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasAdConsent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasAdConsent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FBC_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// rıza geri çekildiyse temizle&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fbclid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fbclid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FBC_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fbclid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Aynı fbclid tekrar geldiyse ilk görülme zamanını koru&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sameClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fbclid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fbclid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstSeen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sameClick&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstSeen&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ms!&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FBC_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;fbclid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;firstSeen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fbc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`fb.1.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstSeen&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fbclid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstSeen&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;TTL_DAYS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;864&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FBC_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMetaIdentifiers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FBC_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Pixel'in yazdığı çerez varsa onu tercih et, yoksa kendi ürettiğimiz&lt;/span&gt;
    &lt;span class="na"&gt;fbc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_fbc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fbc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fbp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_fbp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada bilinçli bir öncelik sırası var: Pixel'in yazdığı &lt;code&gt;_fbc&lt;/code&gt; çerezi varsa onu olduğu gibi kullanıyorum, kendi ürettiğim değeri yalnızca yedek olarak tutuyorum. Böylece tarayıcı ve sunucu olayları aynı &lt;code&gt;fbc&lt;/code&gt; değerini taşıyor. &lt;code&gt;_fbp&lt;/code&gt; için ise kendi değerimi üretmiyorum; Pixel yoksa bu alanı boş bırakmak, rastgele bir değer uydurmaktan daha dürüst.&lt;/p&gt;

&lt;p&gt;TTL meselesine de değinmek istiyorum. Doksan gün bir zorunluluk değil, benim yaygın gördüğüm ve Pixel çerezinin varsayılan ömrüyle uyumlu bir başlangıç noktası. Asıl kural şu: saklama süresini aydınlatma metninizde yazdığınız süreyle tutarlı tutun ve süre dolunca değeri gerçekten silin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taşıma: Form gönderimi ve aynı olay kimliği
&lt;/h2&gt;

&lt;p&gt;Form gönderiminde iki şey olur: tarayıcı Pixel'e &lt;code&gt;Lead&lt;/code&gt; olayı atar, sunucu da aynı başvuruyu CRM'e yazar ve CAPI'ye &lt;code&gt;Lead&lt;/code&gt; gönderir. İkisinin aynı olay olduğunu Meta'ya söylemenin yolu &lt;code&gt;event_id&lt;/code&gt;'dir. Pixel tarafında &lt;code&gt;eventID&lt;/code&gt; olarak, sunucu tarafında &lt;code&gt;event_id&lt;/code&gt; olarak aynı değeri ve aynı olay adını gönderirseniz Meta bunları tekilleştirir. Belgelere göre eşleşme kırk sekiz saatlik pencere içinde aranır ve iki olay birkaç dakika arayla gelirse tarayıcı olayı tercih edilir.&lt;/p&gt;

&lt;p&gt;Pratikte &lt;code&gt;event_id&lt;/code&gt;'yi tarayıcıda üretip form gövdesine ekliyorum, sunucuda da aynı değeri lead kaydına yazıyorum. Gizli alanların yanında &lt;code&gt;event_source_url&lt;/code&gt; için sayfa adresini ve kullanıcı aracısını sunucuya taşımayı unutmamak gerekiyor; &lt;code&gt;website&lt;/code&gt; kaynaklı olaylarda &lt;code&gt;client_user_agent&lt;/code&gt; ve &lt;code&gt;event_source_url&lt;/code&gt; alanları bekleniyor. IP adresini ise tarayıcının söylediğine değil, sunucunun gördüğü bağlantıya (ve güvendiğiniz proxy başlığına) göre alın.&lt;/p&gt;

&lt;p&gt;Formsuz kanallar ayrı bir dert. Proje sayfasındaki "WhatsApp'tan yaz" düğmesine basan kişinin &lt;code&gt;fbc&lt;/code&gt; değeri, mesaj CRM'e düştüğünde ortada yoktur. Bu durumda düğmeye tıklanırken kısa ömürlü bir referans kodu üretip mesaj şablonuna eklemek ve sunucuda bu kodu &lt;code&gt;fbc&lt;/code&gt; ile eşlemek işe yarıyor. Kusursuz değil, kullanıcı kodu silebilir; ama hiç bağlantı kurmamaktan iyidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saklama: fbc kişiye değil, temasa aittir
&lt;/h2&gt;

&lt;p&gt;Aynı kişi önce Meta reklamından, iki hafta sonra arama motorundan, bir ay sonra da yönlendirmeyle gelebilir. &lt;code&gt;fbc&lt;/code&gt; değerini kişi kartındaki tek bir alana yazarsanız ya ilkini ezersiniz ya da sonrakileri kaybedersiniz. Bu yüzden tıklama kimliklerini başvuru düzeyindeki bir temas tablosunda tutuyorum; kişi kartı yalnızca bu temaslara referans veriyor. Gönderimleri de doğrudan değil, bir giden kutusu (outbox) tablosu üzerinden yapıyorum.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Her form/WhatsApp/çağrı başvurusu bir temas satırıdır&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;lead_touch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lead_id&lt;/span&gt;         &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- 'form', 'whatsapp', 'call'&lt;/span&gt;
  &lt;span class="n"&gt;project_code&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- ör. 'LANSMAN-A'&lt;/span&gt;
  &lt;span class="n"&gt;fbc&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                     &lt;span class="c1"&gt;-- fb.1.&amp;lt;ms&amp;gt;.&amp;lt;fbclid&amp;gt;, hash'lenmez&lt;/span&gt;
  &lt;span class="n"&gt;fbp&lt;/span&gt;             &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;client_ip&lt;/span&gt;       &lt;span class="n"&gt;INET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_agent&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source_url&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;form_event_id&lt;/span&gt;   &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                     &lt;span class="c1"&gt;-- Pixel ile ortak event_id&lt;/span&gt;
  &lt;span class="n"&gt;consent_ads&lt;/span&gt;     &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;      &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;purge_after&lt;/span&gt;     &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;               &lt;span class="c1"&gt;-- saklama süresi sonu&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;lead_touch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- CAPI'ye gidecek her olay önce buraya yazılır&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;capi_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lead_id&lt;/span&gt;       &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;touch_id&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;lead_touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_name&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;-- 'Lead', 'QualifiedLead', 'Purchase'&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_time&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- olayın gerçekten olduğu an&lt;/span&gt;
  &lt;span class="n"&gt;action_source&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;       &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- pending/sent/failed/dead&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt;      &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;next_try_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNIQUE (event_name, event_id)&lt;/code&gt; kısıtı küçük ama önemli: CRM'de bir danışman aşamayı yanlışlıkla iki kez "Sözleşme" yapıp geri alırsa, ikinci kayıt kuyruğa hiç girmez. Satış olayının hangi temasa bağlanacağı da bir iş kuralıdır. Ben genellikle satıştan önceki, reklam rızası verilmiş en son Meta temasını seçiyorum; ama bu bir atıf modeli kararıdır ve pazarlama ekibiyle birlikte yazılı hâle getirilmelidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aşama eşlemesi: Her CRM aşaması Meta olayı değildir
&lt;/h2&gt;

&lt;p&gt;Bir gayrimenkul CRM'inde on, on iki aşama olabilir: yeni, ulaşılamadı, arandı, nitelikli, randevu verildi, ziyaret yapıldı, teklif, kapora, sözleşme, iptal. Bunların hepsini Meta'ya göndermek algoritmaya sinyal değil gürültü verir. Benim yaklaşımım üç, en fazla dört olay seçmek: form anında standart &lt;code&gt;Lead&lt;/code&gt;, danışmanın elemesinden geçen kişi için bir özel olay (ör. &lt;code&gt;QualifiedLead&lt;/code&gt;), ziyaret için standart &lt;code&gt;Schedule&lt;/code&gt; ya da özel bir olay, satış için de değerli bir &lt;code&gt;Purchase&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Özel olay adı kullanmak serbesttir ama adın sabit kalması gerekir; bugün &lt;code&gt;QualifiedLead&lt;/code&gt;, yarın &lt;code&gt;Nitelikli_Lead&lt;/code&gt; gönderirseniz Events Manager'da iki ayrı olay görürsünüz. Ayrıca negatif aşamaları (ulaşılamadı, iptal) olay olarak göndermiyorum. İptal edilen bir satışın değerini geri almak için CAPI'de doğrudan bir "geri alma" mekanizması aramak yerine, satış olayını kapora değil sözleşme gibi daha geç ve daha kesin bir aşamaya bağlamayı tercih ediyorum.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CRM aşaması -&amp;gt; Meta olayı eşlemesi (tek yerde, sürüm kontrolünde)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STAGE_EVENT_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lead&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;actionSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;website&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="na"&gt;sendFromServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;qualified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;QualifiedLead&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actionSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system_generated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;site_visit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;actionSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;physical_store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Purchase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;actionSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;physical_store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;withValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// unreachable, lost, cancelled: bilinçli olarak gönderilmiyor&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildEventId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Deterministik: aynı lead + aynı olay her zaman aynı kimliği üretir&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;leadId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;action_source&lt;/code&gt; alanını burada dürüstçe doldurmak önemli. Form &lt;code&gt;website&lt;/code&gt; olayıdır. Danışmanın CRM'de işaretlediği "nitelikli" aşaması bir sistem kararıdır; sözleşme ise satış ofisinde, yüz yüze imzalanır. Meta'nın kabul ettiği değerler arasında &lt;code&gt;website&lt;/code&gt;, &lt;code&gt;phone_call&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt;, &lt;code&gt;physical_store&lt;/code&gt;, &lt;code&gt;system_generated&lt;/code&gt;, &lt;code&gt;business_messaging&lt;/code&gt; ve &lt;code&gt;other&lt;/code&gt; gibi seçenekler var. Değeri gönderim penceresini uzatmak için değil, olayın gerçekte nerede olduğuna göre seçin.&lt;/p&gt;

&lt;p&gt;Form olayı için deterministik kimliği kullanmıyorum; orada Pixel ile ortak olan, tarayıcıda üretilmiş &lt;code&gt;form_event_id&lt;/code&gt; geçerli. Sunucunun ürettiği deterministik kimlikler ise sadece CRM kaynaklı olaylar için.&lt;/p&gt;

&lt;h2&gt;
  
  
  event_time: Kırk beş gün sonraki satış nasıl gönderilir?
&lt;/h2&gt;

&lt;p&gt;En sık karıştırılan nokta burası. &lt;code&gt;event_time&lt;/code&gt;, olayın gerçekten gerçekleştiği anın saniye cinsinden Unix zamanıdır. Meta belgelerine göre bu zaman, gönderim anından en fazla yedi gün öncesi olabilir; isteğin içindeki tek bir olay bile bu sınırı aşarsa isteğin tamamı reddedilir. &lt;code&gt;physical_store&lt;/code&gt; kaynaklı olaylar için bu pencere altmış iki gündür.&lt;/p&gt;

&lt;p&gt;Bu sınır tıklamayla değil, olayla ilgilidir. Kırk beş gün önce reklama tıklamış birinin sözleşmesini, sözleşme imzalandığı gün gönderirseniz &lt;code&gt;event_time&lt;/code&gt; bugündür ve sorun yoktur; &lt;code&gt;fbc&lt;/code&gt; içindeki zaman damgası ise tıklama anını taşımaya devam eder. Sorun, ekibin satışları ay sonunda toplu girdiği yapılarda çıkar: imza tarihi &lt;code&gt;event_time&lt;/code&gt; olarak doğru girilir ama gönderim penceresi kaçmış olur. Bu yüzden iki kural koyuyorum: CRM'de aşama değişikliği anında kuyruğa yazılsın ve kuyruk işçisi pencereyi aşmış olayları göndermeye çalışmak yerine &lt;code&gt;dead&lt;/code&gt; olarak işaretleyip raporlasın. Bir de toplu gönderimde (&lt;code&gt;data&lt;/code&gt; dizisi) eski bir olayın tüm partiyi düşürmemesi için yaşlı olayları partiye hiç almıyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hash'leme ve Türkiye'ye özgü telefon normalizasyonu
&lt;/h2&gt;

&lt;p&gt;E-posta, telefon, ad, soyad, şehir gibi alanlar SHA-256 ile hash'lenerek gönderilir. &lt;code&gt;fbc&lt;/code&gt;, &lt;code&gt;fbp&lt;/code&gt;, &lt;code&gt;client_ip_address&lt;/code&gt; ve &lt;code&gt;client_user_agent&lt;/code&gt; ise hash'lenmez. Hash'lemenin değeri tamamen normalizasyona bağlıdır: aynı e-postayı farklı biçimde hash'lerseniz Meta'nın tarafındaki değerle hiçbir zaman eşleşmez ve hata da almazsınız. Sessiz bir başarısızlıktır.&lt;/p&gt;

&lt;p&gt;Türkiye'deki CRM verisinde telefon numarası en dağınık alandır: &lt;code&gt;0532 123 45 67&lt;/code&gt;, &lt;code&gt;+90 (532) 123-45-67&lt;/code&gt;, &lt;code&gt;5321234567&lt;/code&gt;, &lt;code&gt;0090532...&lt;/code&gt; aynı veritabanında yan yana durur. Meta'nın beklediği biçim, sembolleri ve baştaki sıfırları atılmış, ülke koduyla başlayan rakam dizisidir; yani &lt;code&gt;+90&lt;/code&gt; önekinin artı işareti olmadan &lt;code&gt;90&lt;/code&gt; ile başlayan biçim. Türk harfleriyle ilgili ince bir tuzak da var: JavaScript'te &lt;code&gt;toLocaleLowerCase("tr")&lt;/code&gt; büyük &lt;code&gt;I&lt;/code&gt; harfini &lt;code&gt;ı&lt;/code&gt; yapar ve &lt;code&gt;AHMET.IŞIK@...&lt;/code&gt; gibi bir adresin hash'i bozulur. E-postada yerel ayardan bağımsız küçültme kullanın.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// toLocaleLowerCase('tr') KULLANMA&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeTrPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// sadece rakamlar&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 0090... -&amp;gt; 90...&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 0532... -&amp;gt; 532...&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;90&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// yerel cep&lt;/span&gt;
  &lt;span class="c1"&gt;// Yalnızca ülke kodlu, makul uzunlukta numaraları kabul et&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{11,15}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;em&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeTrPhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;em&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;em&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;em&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ph&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ph&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;external_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))],&lt;/span&gt;
    &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="na"&gt;fbc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;fbc&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// hash'lenmez&lt;/span&gt;
    &lt;span class="na"&gt;fbp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;fbp&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// hash'lenmez&lt;/span&gt;
    &lt;span class="na"&gt;client_ip_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;client_ip&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_user_agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user_agent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;normalizeTrPhone&lt;/code&gt; bilerek tutucu yazıldı: emin olamadığı numarayı &lt;code&gt;null&lt;/code&gt; döndürür. Yanlış normalize edilmiş bir telefonu göndermek, hiç göndermemekten daha kötü değildir ama hata ayıklamayı zorlaştırır; reddedilen numaraları ayrı bir sayaçta tutmak veri kalitesi sorununu görünür kılar. Yurt dışından gelen alıcılar için ülke kodunu tahmin etmek yerine formda ayrı bir ülke kodu seçici kullanmak daha sağlıklı. Ad ve soyad için de Meta küçük harf ve noktalama içermeyen biçim istiyor; Türkçe karakterler UTF-8 olarak kodlanmalı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gönderim: Outbox işçisi, yeniden deneme ve test kodu
&lt;/h2&gt;

&lt;p&gt;CAPI çağrısını CRM'in aşama değiştirme isteğinin içinde yapmak cazip gelir ama Meta tarafındaki kısa bir kesinti ya da token sorunu, danışmanın "kaydet" düğmesini hataya düşürmemelidir. Aşama değişikliği aynı veritabanı işleminde &lt;code&gt;capi_outbox&lt;/code&gt;'a satır yazar, ayrı bir işçi de kuyruğu boşaltır. Uç nokta &lt;code&gt;POST https://graph.facebook.com/&amp;lt;sürüm&amp;gt;/&amp;lt;PIXEL_ID&amp;gt;/events&lt;/code&gt; biçimindedir; sürümü sabit kodlamak yerine yapılandırmadan okuyun ve periyodik olarak güncelleyin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GRAPH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://graph.facebook.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_GRAPH_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PIXEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_PIXEL_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_CAPI_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// yalnızca sunucuda&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;physical_store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flushOutbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claimPending&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// SELECT ... FOR UPDATE SKIP LOCKED&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_S&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action_source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dead&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event_time penceresi aşıldı&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// eski olay tüm partiyi düşürmesin&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_TEST_EVENT_CODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test_event_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META_TEST_EVENT_CODE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;GRAPH&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PIXEL_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/events?access_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fbtrace_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backoffMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;backoffMin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Birkaç ayrıntı: 5xx ve hız sınırı yanıtlarında üstel geri çekilmeyle tekrar deniyorum, 4xx yanıtlarında ise (biçim hatası, geçersiz token, eski &lt;code&gt;event_time&lt;/code&gt;) tekrar denemek anlamsız olduğu için satırı &lt;code&gt;failed&lt;/code&gt; yapıp insana bırakıyorum. Yeniden denemelerde &lt;code&gt;event_id&lt;/code&gt; değişmediği için aynı olayın iki kez gönderilmesi Meta tarafında tekilleştirilir; outbox'ın güvenli olmasını sağlayan şey de bu. Geliştirme ve test ortamında &lt;code&gt;test_event_code&lt;/code&gt; ile Events Manager'daki test ekranını kullanmak, canlı veriyi kirletmeden payload'u doğrulamanın en pratik yolu. Erişim token'ını tarayıcıya, mobil uygulamaya ya da istemci tarafı etiket yöneticisine koymayın.&lt;/p&gt;

&lt;p&gt;Satış olayının &lt;code&gt;payload&lt;/code&gt; kısmında &lt;code&gt;custom_data&lt;/code&gt; içine &lt;code&gt;value&lt;/code&gt; ve &lt;code&gt;currency&lt;/code&gt; (ör. &lt;code&gt;TRY&lt;/code&gt;) eklenir. Gayrimenkulde burada bir iş kararı gerekir: sözleşme bedelinin tamamını mı, şirket için anlamlı bir marj ya da komisyonu mu göndereceksiniz? Tutarlı olduğunuz sürece ikisi de savunulabilir; önemli olan değerin her olayda aynı mantıkla hesaplanmasıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzleme: Event Match Quality ve kendi metrikleriniz
&lt;/h2&gt;

&lt;p&gt;Meta, Events Manager'da her olay için Event Match Quality (EMQ) adında bir eşleşme kalitesi puanı gösterir. Bu puan, gönderdiğiniz müşteri bilgisi parametrelerinin olayları Meta hesaplarıyla ne kadar iyi eşleştirebildiğine dair bir göstergedir. Kesin bir formülü yok, kendi sisteminizden ölçemezsiniz; ama yön gösterir. Puan düşükse genellikle sebep normalizasyon hatası, eksik &lt;code&gt;fbc&lt;/code&gt;/&lt;code&gt;fbp&lt;/code&gt; ya da sunucu olaylarına IP ve kullanıcı aracısının eklenmemesidir.&lt;/p&gt;

&lt;p&gt;EMQ'ya ek olarak kendi tarafımda üç şeyi izliyorum: Meta kaynaklı başvurularda &lt;code&gt;fbc&lt;/code&gt; doluluk oranı, outbox'ta &lt;code&gt;failed&lt;/code&gt;/&lt;code&gt;dead&lt;/code&gt; satır sayısı ve form olaylarında Pixel ile sunucu sayılarının birbirine yakınlığı.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Son 7 gün: kanal bazında fbc doluluğu ve kuyruk sağlığı&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;touches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fbc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fbc_fill_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;failed_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'dead'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;expired_events&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;lead_touch&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;capi_outbox&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;consent_ads&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;touches&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu sorgudaki oranlar için size hedef bir sayı vermeyeceğim; her projenin trafik yapısı farklı. Değerli olan mutlak değer değil, değişimdir. Bir sabah form kanalındaki &lt;code&gt;fbc&lt;/code&gt; doluluğunun dibe vurduğunu görürseniz, büyük ihtimalle biri açılış sayfasını yeniden tasarlarken yakalama betiğini kaldırmıştır ya da bir yönlendirme URL parametrelerini düşürüyordur. Bu hat sessizce bozulur; alarm kurmazsanız haftalar sonra fark edersiniz.&lt;/p&gt;

&lt;h2&gt;
  
  
  KVKK ve rıza üzerine kısa not
&lt;/h2&gt;

&lt;p&gt;Bu bölüm hukuki görüş değildir; kendi projeniz için mutlaka hukuk danışmanınızla çalışın. Teknik açıdan dikkat ettiğim noktalar şunlar: reklam ve ölçüm amaçlı yakalamayı açık rızaya bağlamak ve rıza durumunu temas satırında (&lt;code&gt;consent_ads&lt;/code&gt;) saklamak, rıza yoksa &lt;code&gt;fbc&lt;/code&gt;/&lt;code&gt;fbp&lt;/code&gt; yakalamamak ve CRM olaylarını Meta'ya göndermemek, aydınlatma metninde Meta'ya veri aktarımını ve saklama süresini açıkça belirtmek, &lt;code&gt;purge_after&lt;/code&gt; süresi dolan tıklama kimliklerini gerçekten silmek. Hash'leme veriyi anonim hâle getirmez; hash'lenmiş e-posta hâlâ kişisel veri olarak değerlendirilmelidir. Yurt dışına aktarım boyutu da hukuki değerlendirme gerektiren ayrı bir konudur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık yapılan hatalar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fbc&lt;/code&gt; içindeki zamanı saniye cinsinden yazmak ya da &lt;code&gt;fbclid&lt;/code&gt; değerini küçük harfe çevirmek.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fbc&lt;/code&gt; ve &lt;code&gt;fbp&lt;/code&gt; değerlerini de hash'lemek.&lt;/li&gt;
&lt;li&gt;Pixel ve sunucuda farklı &lt;code&gt;event_id&lt;/code&gt; ya da farklı olay adı kullanıp form olaylarını çift saymak.&lt;/li&gt;
&lt;li&gt;Telefonu &lt;code&gt;0532...&lt;/code&gt; ya da &lt;code&gt;+90...&lt;/code&gt; biçiminde hash'lemek.&lt;/li&gt;
&lt;li&gt;Satışları ay sonunda toplu girip &lt;code&gt;event_time&lt;/code&gt; penceresini kaçırmak.&lt;/li&gt;
&lt;li&gt;Her CRM aşamasını ayrı olay olarak gönderip sinyali gürültüye boğmak.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fbc&lt;/code&gt; değerini kişi kartında tek bir alanda tutup sonraki temaslarla ezmek.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SSS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;fbclid ile _fbc aynı şey mi?&lt;/strong&gt;&lt;br&gt;
Hayır. &lt;code&gt;fbclid&lt;/code&gt; URL'deki ham tıklama kimliğidir; &lt;code&gt;_fbc&lt;/code&gt; ise bu kimliği &lt;code&gt;fb.1.&amp;lt;ms zaman damgası&amp;gt;.&amp;lt;fbclid&amp;gt;&lt;/code&gt; biçiminde, oluşturulma zamanıyla birlikte taşıyan değerdir. CAPI'ye &lt;code&gt;fbc&lt;/code&gt; alanında ham &lt;code&gt;fbclid&lt;/code&gt; değil, bu biçimdeki değer gönderilmelidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pixel çerezini kullanamıyorsam _fbc değerini kendim üretebilir miyim?&lt;/strong&gt;&lt;br&gt;
Evet. Meta, çerez kaydedilmiyorsa &lt;code&gt;fbclid&lt;/code&gt;'in ilk görüldüğü zaman damgasıyla değeri sunucuda üretmeyi ve &lt;code&gt;subdomainIndex&lt;/code&gt; olarak &lt;code&gt;1&lt;/code&gt; kullanmayı öneriyor. &lt;code&gt;_fbp&lt;/code&gt; için ise rastgele değer uydurmak yerine alanı boş bırakmayı tercih ediyorum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead olayını hem Pixel hem CAPI ile göndermek çift sayım yapmaz mı?&lt;/strong&gt;&lt;br&gt;
Aynı &lt;code&gt;event_name&lt;/code&gt; ve aynı &lt;code&gt;event_id&lt;/code&gt; (Pixel tarafında &lt;code&gt;eventID&lt;/code&gt;) kullanılırsa Meta iki olayı tekilleştirir. Kimlikler farklıysa iki ayrı olay olarak sayılır; bu yüzden kimliği tarayıcıda üretip form ile sunucuya taşımak en güvenli yoldur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reklam tıklamasından iki ay sonra imzalanan satışı gönderebilir miyim?&lt;/strong&gt;&lt;br&gt;
Gönderim sınırı tıklamayla değil olay zamanıyla ilgilidir. Satış imzalandığı gün gönderilirse &lt;code&gt;event_time&lt;/code&gt; güncel olur. Pencere, &lt;code&gt;event_time&lt;/code&gt; ile gönderim anı arasındaki farka uygulanır: genel olarak yedi gün, &lt;code&gt;physical_store&lt;/code&gt; olayları için altmış iki gün.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telefon numarasını hangi biçimde hash'lemeliyim?&lt;/strong&gt;&lt;br&gt;
Sembolleri, boşlukları ve baştaki sıfırları atıp ülke koduyla başlayan rakam dizisine çevirin; Türkiye cep numarası için &lt;code&gt;905321234567&lt;/code&gt; biçimi. Artı işareti dahil edilmez. Ardından SHA-256 ile küçük harfli onaltılık (hex) çıktı üretin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fbclid yoksa CAPI göndermeye değer mi?&lt;/strong&gt;&lt;br&gt;
Değer. Eşleşme bu durumda e-posta, telefon, &lt;code&gt;external_id&lt;/code&gt;, IP ve kullanıcı aracısı gibi sinyallere dayanır ve genelde daha zayıf kalır, ama nitelikli lead ve satış olayları yine de algoritmaya ne tür kişilerin gerçekten satışa döndüğünü anlatır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Son söz
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fbclid&lt;/code&gt;'i formdan Meta'ya geri taşımak tek bir kod parçası değil, birbirine bağlı küçük kararlardan oluşan bir zincir: doğru biçimde yakalamak, temasa bağlı saklamak, az ama anlamlı olay seçmek, olay kimliğini korumak, dikkatle normalize etmek ve kuyruğu izlemek. Benim deneyimimde en değerli adım, kod yazmadan önce satış ve pazarlama ekibiyle "hangi aşama bizim için gerçekten başarı" sorusunu netleştirmek oluyor. O cevap belli olduktan sonra geri kalanı, burada anlattığım gibi sıkıcı ve güvenilir bir mühendislik işi.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>crm</category>
      <category>marketing</category>
      <category>javascript</category>
    </item>
    <item>
      <title>GCLID'yi formdan Google Ads'e geri taşımak: Yakalama, saklama, aşama eşlemesi ve offline dönüşüm yükleme</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Mon, 09 Nov 2026 08:26:12 +0000</pubDate>
      <link>https://dev.to/korayyalcin/gclidyi-formdan-google-adse-geri-tasimak-yakalama-saklama-asama-eslemesi-ve-offline-donusum-4f9i</link>
      <guid>https://dev.to/korayyalcin/gclidyi-formdan-google-adse-geri-tasimak-yakalama-saklama-asama-eslemesi-ve-offline-donusum-4f9i</guid>
      <description>&lt;p&gt;Gayrimenkul projesinde bir arama reklamı tıklaması ile tapu devri arasında haftalar, bazen aylar geçer. Reklam paneli o tıklamanın form doldurduğunu bilir ama satışa dönüp dönmediğini bilmez. Satış ofisi de sözleşmeyi bilir ama hangi kampanyadan geldiğini bilmez. Ben Koray Yalçın olarak lead yönetimi ve CRM kurulumlarında bu iki dünyayı birbirine bağlayan en ucuz ve en çok ihmal edilen parçanın GCLID olduğunu düşünüyorum. Sorun genellikle "GCLID'yi kaydedelim" kararının alınması değil; o değerin yolda kaybolması, yanlış kayda bağlanması ya da Google Ads'e yanlış zamanda, yanlış biçimde gönderilmesidir.&lt;/p&gt;

&lt;p&gt;GCLID (Google Click Identifier), Google Ads hesabında otomatik etiketleme açıkken reklam tıklamasıyla gelen ziyaretçinin hedef URL'sine eklenen benzersiz bir sorgu parametresidir. Bu yazıda kavramın tanımında kalmayacağım; bir geliştiricinin kuracağı uçtan uca hattı anlatacağım: tarayıcıda yakalama, form üzerinden taşıma, CRM'de doğru tabloda saklama, satış aşamalarını dönüşüm eylemlerine eşleme ve Google Ads API ile güvenli, tekrarlanabilir yükleme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hattın beş halkası
&lt;/h2&gt;

&lt;p&gt;Bir GCLID'nin değer üretmesi için beş halkanın da sağlam olması gerekir:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Yakalama:&lt;/strong&gt; URL'deki &lt;code&gt;gclid&lt;/code&gt; (ve iOS tarafındaki &lt;code&gt;gbraid&lt;/code&gt; / &lt;code&gt;wbraid&lt;/code&gt;) değerini ilk sayfa görüntülemesinde okumak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kalıcılık:&lt;/strong&gt; Ziyaretçi formu ilk sayfada değil, üçüncü sayfada ya da ertesi gün doldurabilir; değer oturumlar arasında korunmalı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Taşıma:&lt;/strong&gt; Form gönderiminde değer gizli alanla sunucuya gelmeli; WhatsApp ya da telefon gibi formsuz kanallarda da bir köprü kurulmalı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saklama:&lt;/strong&gt; CRM'de değer kişi kartına değil, başvuru (touch) olayına bağlanmalı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geri bildirim:&lt;/strong&gt; Satış aşaması değiştiğinde ilgili dönüşüm eylemi, doğru zaman damgası ve değerle Google Ads'e yüklenmeli.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ekiplerin çoğu birinci ve beşinci halkayı kurar, aradakileri varsayar. Kayıp tam da aradaki halkalarda yaşanır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yakalama ve kalıcılık: İlk sayfada oku, makul süre sakla
&lt;/h2&gt;

&lt;p&gt;Google etiketi (gtag) ve dönüşüm bağlayıcı zaten tıklama bilgisini birinci taraf çerezde tutar. Ama CRM'e gidecek formun bu değere güvenilir biçimde erişmesi için kendi küçük katmanımı yazmayı tercih ediyorum. Bunun iki nedeni var: değeri hangi koşulda sakladığımı kontrol etmek ve rıza durumuna göre davranabilmek.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CLICK_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gclid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gbraid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wbraid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TTL_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;captureClickIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasAdConsent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasAdConsent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// rıza yoksa saklama&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;CLICK_KEYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;landing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;capturedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                               &lt;span class="c1"&gt;// en yeni tıklama kazanır&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readClickId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capturedAt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;TTL_DAYS&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada üç bilinçli karar var. Birincisi, "en yeni tıklama kazanır" kuralı: aynı kişi iki farklı kampanyadan gelirse son tıklamayı tutuyorum, çünkü form gönderimine en yakın niyet odur. İlk tıklamayı da görmek istiyorsanız ikinci bir anahtarda ayrıca saklayın, ama yükleme tarafında yalnızca birini kullanın. İkincisi, süre sınırı: Google Ads çevrimdışı içe aktarmada tıklamanın, dönüşüm eyleminin tıklama sonrası dönüşüm penceresi içinde olması gerekir ve bu pencere en fazla 90 gündür. Pencere dışında kalacak bir değeri taşımak yalnızca hata satırı üretir. Üçüncüsü, rıza kontrolü: reklam ölçümü için rıza alınmamış bir ziyaretçinin tıklama kimliğini saklamıyorum. KVKK ve GDPR açısından nasıl bir rıza metni gerektiği ayrı bir hukuki değerlendirmedir; bu yazı hukuki görüş değildir, ama mimari olarak "rıza yoksa yakalama yok" anahtarının kodda var olması gerekir.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gbraid&lt;/code&gt; ve &lt;code&gt;wbraid&lt;/code&gt; notu: iOS ortamında bazı tıklamalarda GCLID yerine bu parametreler gelir. Google Ads API'deki tıklama dönüşümü nesnesinde bu üç alandan yalnızca biri doldurulabilir. Bu yüzden değeri &lt;code&gt;key&lt;/code&gt; ile birlikte saklıyorum; yükleme sırasında hangi alana yazılacağını bu belirler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taşıma: Gizli alan yetmez, sunucu da doğrulamalı
&lt;/h2&gt;

&lt;p&gt;Formda gizli bir alan açıp değeri oraya koymak işin kolay kısmı. Zor kısmı, sunucunun bu değere körü körüne güvenmemesi. Gizli alan istemciden gelir; boş, bozuk ya da başka bir sayfadan kopyalanmış olabilir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#lead-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readClickId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click_id_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click_id_value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click_landing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landing&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sunucuda yaptığım kontroller basit: anahtar izin verilen üç değerden biri mi, değer makul uzunlukta ve yalnızca URL güvenli karakterlerden mi oluşuyor, aynı değer kısa sürede çok sayıda farklı başvuruda görünüyor mu. Sonuncusu önemli: tek bir GCLID'nin bir saat içinde onlarca farklı telefon numarasıyla gelmesi, ya bir bot trafiğine ya da paylaşılan bir linke işaret eder. Böyle bir değeri dönüşüm yüklemesinden hariç tutmak, reklam algoritmasını kirletmekten iyidir.&lt;/p&gt;

&lt;p&gt;Formsuz kanallarda durum farklı. Ziyaretçi "WhatsApp'tan yaz" butonuna bastığında URL'de GCLID olsa bile mesaj uygulamasına geçince kaybolur. Burada kullandığım yöntem, butona basıldığında sunucuda kısa ömürlü bir referans kodu üretmek, bu kodu tıklama kimliğiyle eşleyip hazır mesaj metnine eklemektir. İlk mesaj geldiğinde entegrasyon kodu çözer ve başvuruya bağlar. Telefon aramalarında ise çağrı takibi tarafındaki numara havuzu benzer bir köprü görevi görür. Her iki yöntemde de amaç aynıdır: tıklama kimliğini insan kanalına geçerken düşürmemek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saklama: GCLID kişiye değil, başvuruya aittir
&lt;/h2&gt;

&lt;p&gt;En sık gördüğüm veri modeli hatası, &lt;code&gt;gclid&lt;/code&gt; alanını kişi (contact) tablosuna tek bir sütun olarak eklemektir. Aynı kişi önce Google'dan, sonra Meta'dan, sonra yine Google'dan gelir; tek sütun her seferinde ezilir ve hangi satışın hangi tıklamaya ait olduğu bilgisi kaybolur. Mükerrer kişi birleştirme işlemi de bu değeri sessizce siler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;lead_touch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;contact_id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;deal_id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- google_ads, meta, portal, organik&lt;/span&gt;
  &lt;span class="n"&gt;click_id_key&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;click_id_key&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gclid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'gbraid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'wbraid'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;click_id_value&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;landing_path&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;clicked_at&lt;/span&gt;      &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;submitted_at&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ad_consent&lt;/span&gt;      &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;lead_touch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;click_id_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;click_id_value&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fırsat (deal) açıldığında hangi başvurunun "kaynak başvuru" olduğunu açıkça işaretliyorum. Genellikle fırsatı doğuran son başvurudur, ama satış ekibi manuel olarak değiştirebilir ve bu değişiklik denetim kaydına düşer. Böylece "bu satışı hangi tıklamaya bağladık ve neden" sorusunun cevabı her zaman sorgulanabilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aşama eşlemesi: Her CRM aşaması bir dönüşüm değildir
&lt;/h2&gt;

&lt;p&gt;Google Ads tarafında birden fazla dönüşüm eylemi tanımlamak, satış hunisinin farklı derinliklerini algoritmaya öğretmenin yoludur. Ama her CRM aşamasını dönüşüme çevirmek gürültü üretir. Gayrimenkul satışında anlamlı bulduğum eşleme şöyle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CRM aşaması&lt;/th&gt;
&lt;th&gt;Dönüşüm eylemi&lt;/th&gt;
&lt;th&gt;Değer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nitelikli lead&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qualified_lead&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sabit küçük değer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randevu gerçekleşti&lt;/td&gt;
&lt;td&gt;&lt;code&gt;site_visit_done&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sabit orta değer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapora alındı&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deposit_paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kapora tutarı veya sabit değer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sözleşme imzalandı&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contract_signed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sözleşme bedeli&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Değerlerin ne olacağı işletmenin kendi ekonomisine bağlıdır; burada rakam vermiyorum, çünkü her projenin bilet büyüklüğü ve dönüşüm zinciri farklıdır. Önemli olan kural: bu eşleme kodda sabit değil, veri olarak tutulmalı. Pazarlama ekibi yeni bir eylem eklediğinde dağıtım yapmak gerekmemeli.&lt;/p&gt;

&lt;p&gt;İkinci kural, aşama geri gittiğinde ne olacağıdır. Kapora iade edildiyse ya da sözleşme feshedildiyse, daha önce yüklenen dönüşüm için Google Ads'in dönüşüm düzeltme (adjustment) mekanizmasıyla geri çekme veya değer güncelleme gönderilebilir. Bunu yapabilmek için ilk yüklemede tekil bir &lt;code&gt;order_id&lt;/code&gt; kullanmak şarttır; düzeltme o kimlik üzerinden eşleşir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yükleme: Kuyruk, tekil kimlik, kısmi hata
&lt;/h2&gt;

&lt;p&gt;Aşama değişikliği bir olay üretir; olay doğrudan API çağrısı yapmaz, bir giden kutusu (outbox) tablosuna yazılır. Ayrı bir işçi bu tabloyu okuyup toplu yükleme yapar. Bu yapı, Google Ads API geçici olarak hata verdiğinde satış ekibinin ekranını kilitlemez ve her yüklemenin izini tutar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.ads.googleads.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GoogleAdsClient&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_click_conversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ClickConversion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;click_id_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;click_id_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# gclid / gbraid / wbraid
&lt;/span&gt;    &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConversionActionService&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;conversion_action_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_action_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Biçim: "yyyy-mm-dd hh:mm:ss+hh:mm", saat dilimi açık olmalı
&lt;/span&gt;    &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_date_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;converted_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S%z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_date_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_date_time&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_date_time&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversion_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;
    &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;            &lt;span class="c1"&gt;# tekilleştirme ve sonraki düzeltme için
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conv&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConversionUploadService&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UploadClickConversionsRequest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;build_click_conversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partial_failure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_click_conversions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu kodda dikkat ettiğim ayrıntılar şunlar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zaman damgası dönüşüm anıdır, yükleme anı değil.&lt;/strong&gt; Sözleşmenin imzalandığı zaman yazılır. Saat dilimi açıkça belirtilmezse dönüşüm, tıklamadan önceymiş gibi görünüp reddedilebilir. Kıbrıs veya Türkiye saatinde çalışan bir satış ofisinde bu hata beklenenden sık çıkar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;partial_failure&lt;/code&gt; açık.&lt;/strong&gt; Toplu yüklemede tek bir bozuk satır tüm paketi düşürmemeli. Yanıttaki kısmi hata ayrıntısı satır satır okunur ve outbox kaydına yazılır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;order_id&lt;/code&gt; deterministik.&lt;/strong&gt; Aynı fırsat ve aynı eylem için her zaman aynı kimlik üretilir, örneğin &lt;code&gt;deal_id&lt;/code&gt; ile eylem adının birleşimi. İşçi çöküp aynı satırı yeniden gönderse bile çift sayım oluşmaz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Para birimi açık.&lt;/strong&gt; Sterlin bazlı satış yapan bir projede değeri liraya çevirip göndermek ya da para birimini boş bırakmak, raporlamada sessiz bir kaymaya yol açar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hata sınıflandırması da önemlidir. Tıklamanın bulunamaması, pencerenin geçmiş olması ya da dönüşümün tıklamadan önce görünmesi kalıcı hatalardır; yeniden denenmez, raporlanır. Kota veya geçici sunucu hataları ise geri çekilmeli yeniden deneme ile tekrar kuyruğa alınır. İki sınıfı karıştırmak, ya kalıcı hatalarla kuyruğu tıkar ya da geçici hatalarda veri kaybeder.&lt;/p&gt;

&lt;h2&gt;
  
  
  GCLID olmadığında: Gelişmiş dönüşümler bir yedek
&lt;/h2&gt;

&lt;p&gt;Her başvuruda tıklama kimliği olmayacak. Rıza verilmemiş olabilir, ziyaretçi başka cihazdan dönmüş olabilir, kanal köprüsü kopmuş olabilir. Google'ın potansiyel müşteriler için gelişmiş dönüşümler (enhanced conversions for leads) özelliği, normalize edilip SHA-256 ile özetlenmiş e-posta veya telefon üzerinden eşleşme yapmaya izin verir. Ben bunu GCLID'nin yerine değil, yanına konmuş bir yedek olarak görüyorum. Normalizasyon kuralları (küçük harf, boşluk temizleme, telefonda uluslararası biçim) burada da kritik; eşleşme oranını en çok etkileyen şey özetlemeden önceki temizliktir. Bu tür kişisel veri işleme için de rıza ve aydınlatma tarafı ayrıca değerlendirilmelidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzleme: Hat sessizce bozulur
&lt;/h2&gt;

&lt;p&gt;Bu hattın en tehlikeli özelliği, bozulduğunda kimsenin fark etmemesidir. Bir tema güncellemesi gizli alanı formdan kaldırır, bir yönlendirme kuralı sorgu parametrelerini düşürür, bir CRM kullanıcısı aşama adını değiştirir ve eşleme tablosu boşa düşer. Bu yüzden haftalık değil günlük baktığım birkaç gösterge var:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Ads kanalından gelen başvurularda tıklama kimliği dolu olanların oranı. Ani düşüş genellikle yakalama veya taşıma halkasının koptuğunu gösterir.&lt;/li&gt;
&lt;li&gt;Outbox'ta bekleyen, başarılı, kalıcı hatalı ve yeniden denenen satır sayıları.&lt;/li&gt;
&lt;li&gt;Kalıcı hataların neden dağılımı. Aynı neden birden artıyorsa kod değişmiş demektir.&lt;/li&gt;
&lt;li&gt;Yüklenen dönüşüm sayısı ile CRM'deki aynı aşamadaki fırsat sayısı arasındaki fark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Son madde bir mutabakat kontrolüdür ve en değerlisidir. Reklam panelindeki sayıyla CRM'deki sayı hiçbir zaman birebir tutmaz, ama farkın nedenlerini (rıza yok, pencere dışı, kimlik yok) açıklayabiliyorsanız hat sağlıklıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık yapılan hatalar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GCLID'yi yalnızca açılış sayfasında okuyup formun başka sayfada olduğunu unutmak.&lt;/li&gt;
&lt;li&gt;Değeri kişi kartında tek sütunda tutmak ve her yeni başvuruda ezmek.&lt;/li&gt;
&lt;li&gt;Yükleme zamanını dönüşüm zamanı olarak göndermek.&lt;/li&gt;
&lt;li&gt;Saat dilimi bilgisini atlamak.&lt;/li&gt;
&lt;li&gt;Tekil &lt;code&gt;order_id&lt;/code&gt; kullanmadan yeniden deneme yapmak.&lt;/li&gt;
&lt;li&gt;Her CRM aşamasını ayrı dönüşüm eylemi yapıp algoritmaya gürültü vermek.&lt;/li&gt;
&lt;li&gt;İade ve fesihleri geri yansıtmamak.&lt;/li&gt;
&lt;li&gt;Rıza durumunu yakalama anında değil, yalnızca yükleme anında kontrol etmek.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sık Sorulan Sorular (SSS)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GCLID'yi neden kişi tablosunda değil de başvuru tablosunda tutmalıyım?&lt;/strong&gt;&lt;br&gt;
Çünkü bir kişi birden fazla kampanyadan, farklı zamanlarda gelebilir. Tıklama kimliği bir başvuru olayının özelliğidir. Kişi tablosunda tutulduğunda her yeni başvuru eskisini ezer ve satışın gerçek kaynağı kaybolur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Satış 90 günden sonra kapanırsa ne olur?&lt;/strong&gt;&lt;br&gt;
Tıklama, dönüşüm eyleminin tıklama sonrası penceresinin dışında kalıyorsa Google Ads o dönüşümü kabul etmez. Uzun satış döngülerinde bu yüzden randevu veya kapora gibi daha erken, pencere içinde kalan aşamaları da dönüşüm eylemi olarak tanımlamak mantıklıdır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aynı dönüşümü yanlışlıkla iki kez yüklersem ne olur?&lt;/strong&gt;&lt;br&gt;
Her yüklemede aynı fırsat ve eylem için aynı &lt;code&gt;order_id&lt;/code&gt; değerini gönderiyorsanız tekrar eden satır tekilleştirilir. &lt;code&gt;order_id&lt;/code&gt; göndermiyorsanız çift sayım riski vardır; bu yüzden deterministik kimlik üretimi zorunlu kabul edilmelidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kapora iade edildiğinde dönüşümü nasıl geri alırım?&lt;/strong&gt;&lt;br&gt;
Google Ads'in dönüşüm düzeltme mekanizmasıyla, ilk yüklemedeki &lt;code&gt;order_id&lt;/code&gt; üzerinden geri çekme veya değer güncelleme gönderilebilir. Bunu da outbox üzerinden, ayrı bir olay türü olarak işlemek en temiz yoldur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gbraid ve wbraid ile ne yapmalıyım?&lt;/strong&gt;&lt;br&gt;
GCLID ile aynı şekilde yakalayıp hangi anahtarla geldiğini birlikte saklayın. Yükleme sırasında tıklama dönüşümü nesnesinde bu üç alandan yalnızca biri doldurulabilir; hangisinin kullanılacağını saklanan anahtar belirler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rıza vermeyen ziyaretçi için ne yapılır?&lt;/strong&gt;&lt;br&gt;
Tıklama kimliği saklanmaz ve yüklenmez. Bu başvurular CRM'de normal şekilde işlenir, yalnızca reklam geri bildirim hattına girmez. Rıza metninin içeriği ve hukuki dayanağı için uzman görüşü alınmalıdır; bu yazı hukuki tavsiye değildir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bu hattı kurmak için mutlaka Google Ads API mi kullanmalıyım?&lt;/strong&gt;&lt;br&gt;
Küçük hacimlerde panelden dosya ile içe aktarma da mümkündür. Ancak aşama değişiklikleri sürekli olduğunda ve düzeltme, yeniden deneme, mutabakat gerektiğinde API tabanlı bir kuyruk çok daha güvenilir ve izlenebilir bir yapı sunar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Son söz
&lt;/h2&gt;

&lt;p&gt;GCLID küçük bir sorgu parametresi gibi görünür ama reklam bütçesinin satışa nasıl dönüştüğünü algoritmaya öğreten tek sağlam bağdır. Yakalama, kalıcılık, taşıma, saklama ve geri bildirim halkalarını ayrı ayrı tasarlayıp izlediğinizde, reklam paneli yalnızca form dolduranları değil, gerçekten satın alanları anlamaya başlar. Lead yönetimi ve CRM entegrasyonu üzerine çalışırken en çok önem verdiğim ilke de bu: veri, kaybolduğu yerde değil, doğduğu yerde korunmalıdır.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Koray Yalçın — lead yönetimi, CRM mimarisi, dijital pazarlama ve PropTech üzerine yazıyor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>crm</category>
      <category>marketing</category>
      <category>google</category>
    </item>
    <item>
      <title>Lead deduplication'ı kodla kurmak: Blocking key, telefon normalizasyonu, eşik ve kişi grafı</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Sun, 08 Nov 2026 19:18:07 +0000</pubDate>
      <link>https://dev.to/korayyalcin/lead-deduplicationi-kodla-kurmak-blocking-key-telefon-normalizasyonu-esik-ve-kisi-grafi-73g</link>
      <guid>https://dev.to/korayyalcin/lead-deduplicationi-kodla-kurmak-blocking-key-telefon-normalizasyonu-esik-ve-kisi-grafi-73g</guid>
      <description>&lt;p&gt;Lansman haftasında aynı kişi üç kanaldan geliyor: sabah Meta anlık formunu dolduruyor, öğleden sonra proje sitesindeki formu gönderiyor, akşam da ilan portalından "bilgi al" butonuna basıyor. CRM'de üç kayıt açılıyor, üç danışman aynı numarayı arıyor, aday kızıyor, pazarlama ekibi de "reklam çalışıyor ama satış kaçırıyoruz" diyor. Ben Koray Yalçın olarak lead yönetimi ve CRM kurulumlarında bu tabloyu defalarca gördüm: mükerrer kayıt, bir temizlik işi gibi düşünülüyor; oysa sorun kayıt geldikten haftalar sonra tabloda satır birleştirmek değil, kaydın sisteme girdiği anda kimliğin çözülmemesidir.&lt;/p&gt;

&lt;p&gt;Lead deduplication (mükerrer lead ayıklama), yeni bir talebin mevcut bir kişiye ait olup olmadığına karar veren ve bu kararı tekrarlanabilir kurallarla veren katmandır. Kavramın kısa tanımını &lt;a href="https://www.korayyalcin.org/yayinlar-arastirmalar/sozluk/lead-deduplication/" rel="noopener noreferrer"&gt;lead deduplication nedir&lt;/a&gt; sözlük maddesinde tutuyorum; bu yazıda o tanımı bir geliştiricinin kuracağı yapıya çeviriyorum: normalizasyon, blocking key, kural tablosu, üç sonuçlu karar (bağla / incelemeye al / yeni kişi aç) ve geri alınabilir bir kişi grafı.&lt;/p&gt;

&lt;p&gt;Önemli bir ayrım var. Her gelen formu silip tek satırda toplamak istemiyoruz. Her başvuru bir niyet sinyalidir: hangi kanal, hangi proje, hangi saat, hangi reklam. Birleştirilmesi gereken şey kişi kimliğidir, başvuru olayı değil.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telefon tek başına yetmez
&lt;/h2&gt;

&lt;p&gt;Birçok ekip "aynı telefon varsa aynı kişidir" kuralıyla yola çıkar. Gayrimenkul tarafında bu kural üç şekilde bozulur.&lt;/p&gt;

&lt;p&gt;Birincisi biçim. &lt;code&gt;0532 111 22 33&lt;/code&gt;, &lt;code&gt;+90 532 111 22 33&lt;/code&gt;, &lt;code&gt;00905321112233&lt;/code&gt; ve &lt;code&gt;5321112233&lt;/code&gt; aynı numaradır ama metin olarak dört farklı değerdir. İkincisi paylaşım. Bir ofis sabit hattı veya bir ev numarası iki farklı kişi tarafından kullanılabilir; özellikle ailece bakılan bir dairede bu sık görülür. Üçüncüsü değişim. Aynı kişi yeni bir hatla ikinci kez başvurur; telefon eşleşmez, e-posta eşleşir.&lt;/p&gt;

&lt;p&gt;Bu yüzden eşleştirmeyi tek bir alana bağlamak yerine, normalize edilmiş birkaç kimlik anahtarı ve bu anahtarların güven düzeyini tutan kurallar kullanıyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Önce normalize et, sonra karşılaştır
&lt;/h2&gt;

&lt;p&gt;Karşılaştırma asla ham metin üzerinde yapılmamalı. Telefon için hedef biçim E.164'tür; Türkiye'de mobil numarada baştaki sıfır ve &lt;code&gt;0090&lt;/code&gt; / &lt;code&gt;+90&lt;/code&gt; varyantları tek bir &lt;code&gt;+90XXXXXXXXXX&lt;/code&gt; değerine indirgenir. E-posta için küçük harf, uç boşluk ve Gmail'deki nokta ile &lt;code&gt;+&lt;/code&gt; takıları ayrı ele alınır: &lt;code&gt;ali.veli+proje@gmail.com&lt;/code&gt; ile &lt;code&gt;aliveli@gmail.com&lt;/code&gt; aynı teslim kutusudur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhoneNumberFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;E164&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;phonenumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumberParseException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;googlemail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İsim için daha temkinliyim. Türkçe karakterleri NFC ile tutmak, fazla boşlukları sıkıştırmak ve unvanları (&lt;code&gt;sn.&lt;/code&gt;, &lt;code&gt;mr.&lt;/code&gt;) atmak yeterlidir. İsmi tek başına eşleşme anahtarı yapmam; "Mehmet Yılmaz" aynı projede onlarca kez geçer. İsim, telefon veya e-posta gibi güçlü bir anahtarla birlikte destekleyici kanıt olur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kişi grafı: başvuruyu silme, kişiye bağla
&lt;/h2&gt;

&lt;p&gt;Şemada üç tablo tutuyorum. &lt;code&gt;person&lt;/code&gt; kanonik kimliktir. &lt;code&gt;lead_event&lt;/code&gt; her başvurudur ve asla güncellenerek yok edilmez. &lt;code&gt;person_identifier&lt;/code&gt; o kişiye bağlı, normalize edilmiş anahtarlardır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;lead_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;               &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;person_id&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;source&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source_record_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;project_key&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;          &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt;      &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;match_status&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;match_rule&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;match_score&lt;/span&gt;      &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_record_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;person_identifier&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;person_id&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;value&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;first_seen&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;match_review&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;                  &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lead_event_id&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;lead_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;candidate_person_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;score&lt;/span&gt;               &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reasons&lt;/span&gt;             &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;          &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;resolved_at&lt;/span&gt;         &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolution&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNIQUE (kind, value)&lt;/code&gt; satırı kritiktir: aynı E.164 numarası iki kişiye bağlanamaz. İki kişi aynı numarayı gerçekten paylaşıyorsa bu bir inceleme kaydıdır, sessizce ikinci kişiye yazılmaz. &lt;code&gt;UNIQUE (source, source_record_id)&lt;/code&gt; ise aynı Meta form gönderiminin iki kez işlenmesini engeller; bu, deduplication'ın kardeşi olan idempotent ingest'tir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocking key: tüm tabloyu taramayın
&lt;/h2&gt;

&lt;p&gt;Her yeni lead geldiğinde bütün &lt;code&gt;person&lt;/code&gt; tablosunu gezmek hem yavaş hem tehlikelidir; yanlış eşleşme riski hacimle birlikte artar. Blocking, olası adayları ucuz bir anahtarla daraltır, pahalı karşılaştırmayı yalnız o kümeye uygular.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;person_block&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;block_key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- örnek anahtarlar:&lt;/span&gt;
&lt;span class="c1"&gt;-- phone:+905321112233&lt;/span&gt;
&lt;span class="c1"&gt;-- email:aliveli@gmail.com&lt;/span&gt;
&lt;span class="c1"&gt;-- phone_last7:5321112233&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yeni kayıt geldiğinde önce kendi blocking key'lerini üretiyorum, &lt;code&gt;person_block&lt;/code&gt; üzerinden aday kişi kimliklerini çekiyorum, sonra yalnız bu adaylar üzerinde kural tablosunu çalıştırıyorum. Hacim binlerle ifade edildiğinde bu fark, tam taramayla kıyaslanamayacak kadar belirgindir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kuralları koda gömmeyin, tabloya yazın
&lt;/h2&gt;

&lt;p&gt;Eşleşme kurallarının iş birimiyle konuşulabilir olması gerekir. "Aynı normalize telefon kesin eşleşmedir" kuralını bir &lt;code&gt;if&lt;/code&gt; dalının içine gömersem, pazarlama ekibi kuralı değiştirmemi istediğinde deploy beklemek zorunda kalır. Ben kuralları veri olarak tutuyorum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;match_rule&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;priority&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;decision&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;min_score&lt;/span&gt;  &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- phone_exact:  {"phone": "exact"} -&amp;gt; auto_link&lt;/span&gt;
&lt;span class="c1"&gt;-- email_exact:  {"email": "exact"} -&amp;gt; auto_link&lt;/span&gt;
&lt;span class="c1"&gt;-- phone_name:   {"phone": "exact", "name": "similar"} -&amp;gt; review&lt;/span&gt;
&lt;span class="c1"&gt;-- email_project:{"email_local": "exact", "project": "exact"} -&amp;gt; review&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kesin telefon veya kesin e-posta eşleşmesi otomatik bağlanır. İsim benzerliği, aynı proje, aynı cihaz veya aynı oturum gibi zayıf sinyaller tek başlarına otomatik birleştirme yapmaz; inceleme kuyruğuna düşer. Yapay zekâyı bu katmanda skor üretmek veya kuyruğu sıralamak için kullanabilirsiniz, birleştirme kararını ona bırakmam. Yanlış birleştirmenin maliyeti, yanlış ayrı tutmanın maliyetinden daha yüksektir: iki gerçek kişinin sohbet geçmişi, izni ve satış aşaması birbirine karışır ve geri almak zordur.&lt;/p&gt;

&lt;p&gt;İsim benzerliği için Levenshtein ya da Jaro-Winkler yeterli olur; eşik değerini kendi verinize bakarak seçin, ezbere bir sayı yazmayın. Türkçe'de &lt;code&gt;ı/i&lt;/code&gt; ve &lt;code&gt;ğ/g&lt;/code&gt; dönüşümlerini karşılaştırmadan önce uygulamak, sahte farkları azaltır. Yine de isim skorunu otomatik birleştirme eşiğinin üstüne koymuyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Karar anı ve yarış durumu
&lt;/h2&gt;

&lt;p&gt;İki form aynı saniye içinde gelebilir. İkisi de henüz &lt;code&gt;person&lt;/code&gt; tablosunda olmayan aynı telefonu taşırsa, kontrol-sonra-yaz yaklaşımı iki kişi üretir. Bunu önlemenin yolu, identifier üzerine konan benzersiz kısıt ve kısa bir kilit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest_lead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_e164&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_norm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_advisory_xact_lock(hashtext(%s))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO person (display_name) VALUES (%s) RETURNING id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO lead_event (
                   person_id, source, source_record_id, project_key,
                   payload, match_status, match_rule, match_score
               ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
               ON CONFLICT (source, source_record_id) DO NOTHING&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO person_identifier (person_id, kind, value)
                       VALUES (%s, %s, %s)
                       ON CONFLICT (kind, value) DO NOTHING&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;queue_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_advisory_xact_lock&lt;/code&gt; aynı telefon veya e-posta üzerinde eşzamanlı iki işlemi sıraya koyar. &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; aynı kaynak kaydının yeniden oynatılmasını yutar. Identifier çakışması olursa (başka bir kişi o telefonu zaten taşıyorsa) bunu sessizce geçmek yerine inceleme kuyruğuna yazın; aksi halde veri kaybını fark etmezsiniz.&lt;/p&gt;

&lt;p&gt;İnceleme kuyruğundaki kayıt için &lt;code&gt;person_id&lt;/code&gt; boş bırakılabilir ya da geçici bir kişi açılıp sonra bağlanabilir. Ben boş bırakmayı tercih ediyorum: satır görünür, danışmana henüz ikinci bir kişi olarak atanmaz, karar verilince bağlanır. Atama ve ilk temas bu kuyruğu bekleyemezse, geçici kişiyi açıp "olası mükerrer" bayrağı koymak daha güvenlidir; aksi halde inceleme bekleyen lead sessizce soğur. İkisinden birini bilinçli seçin.&lt;/p&gt;

&lt;p&gt;Otomatik bağlama, kaydın sahibini değiştirmez. Var olan kişinin bir danışmanı varsa yeni başvuru o kişiye eklenir; sahiplik devredilmez, SLA saati sıfırlanmaz, "yeni lead" kuyruğuna ikinci kez düşülmez. Aksi halde aynı aday her formda başka bir danışmana gider ve mükerreri teknik olarak birleştirsek bile operasyonda yine üç kişi arar. İsterseniz bağlama olayına "mevcut sahibe haber ver" gibi bir yan etki ekleyin; haber vermek ile yeniden atamak farklı işlerdir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Birleştirmeyi geri alınabilir tutun
&lt;/h2&gt;

&lt;p&gt;Otomatik bağlama bile yanlış olabilir. Evli bir çift aynı e-posta kutusunu paylaşıyor olabilir; bir asistan kendi telefonundan işvereni adına form doldurmuş olabilir. Bu yüzden bağlama bir olaydır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;person_link_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lead_event_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;lead_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;from_person_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;to_person_id&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;rule_id&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;actor&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Geri alma (&lt;code&gt;unlink&lt;/code&gt;) lead olayını eski haline döndürür, identifier'ı gerekirse koparır ve sonraki otomatik kuralların aynı hatayı hemen tekrarlamaması için bir istisna kaydı bırakır. Denetimde "bu iki kayıt neden birleşti?" sorusunun cevabı bu tablodadır.&lt;/p&gt;

&lt;p&gt;İzin birleştirilirken de dikkat gerekir. Bir kayıttaki geri çekme, diğer kayıttaki daha eski bir onayın üzerine yazılmalıdır; aksi halde mükerreri birleştirirken birine tekrar mesaj göndermiş olursunuz. Bu, izni ayrı bir olay defteri olarak tutmanın bir başka nedenidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ne ölçülür?
&lt;/h2&gt;

&lt;p&gt;Panoda dört sayıya bakıyorum. Otomatik bağlanan başvuruların oranı, inceleme kuyruğunun medyan bekleme süresi, incelemede "yeni kişidir" denilen kayıtların oranı ve geri alınan bağlama sayısı. İlki yüzde yüze yaklaşırsa kurallar çok agresiftir. İkincisi saatleri aşarsa lansman gününde kuyruk satışın önüne geçer. Üçüncüsü çok yüksekse blocking key fazla geniş tutulmuş demektir. Dördüncüsü sıfırdan büyükse, hangi kuralın yanlış pozitif ürettiğini &lt;code&gt;person_link_event&lt;/code&gt; üzerinden geriye doğru okuyabilirsiniz.&lt;/p&gt;

&lt;p&gt;Sahte bir doğruluk yüzdesi uydurmanın alemi yok; bu sayılar sizin verinizde, sizin kurallarınızla anlam kazanır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gayrimenkulde sık gördüğüm tuzaklar
&lt;/h2&gt;

&lt;p&gt;İlan portallarından gelen bildirim e-postalarında telefon bazen imzada, bazen konu satırında, bazen de farklı bir biçimde durur. Ayrıştırıcının ürettiği ham metni doğrudan eşleştirmek yerine önce normalizasyondan geçirin; aksi halde aynı kişi her portal için yeni kayıt olur.&lt;/p&gt;

&lt;p&gt;İkinci tuzak, "mükerrer = değersiz" varsayımıdır. Aynı kişinin üç gün içinde ikinci kez form doldurması genelde çöp değil, ısınma sinyalidir. Lead olayını silmek bu sinyali yok eder. Kişi aynı kalır, olay ayrıca durur, skorlama ve danışman notu bu ikinci olayı görür.&lt;/p&gt;

&lt;p&gt;Üçüncü tuzak, test ve sahte kayıtlardır. &lt;code&gt;555&lt;/code&gt; ile başlayan numaralar, &lt;code&gt;test@&lt;/code&gt; e-postaları ve kendi çalışanlarınızın tekrarlayan başvuruları eşleştirme istatistiklerini bozar. Bunları deduplication'dan önce ayrı bir doğrulama katmanında ayıklamak, hem kuyruğu hem de kuralların kalibrasyonunu temiz tutar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık sorulan sorular
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yalnızca telefon eşleştirsem olmaz mı?&lt;/strong&gt;&lt;br&gt;
Küçük bir ofiste ve tek kanallı bir akışta bir süre idare eder. Kanal sayısı artınca biçim farkları, paylaşılan hatlar ve ikinci numaralar boşluk bırakır. Telefonu en güçlü anahtar olarak tutun, ama tek anahtar yapmayın.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Olası eşleşmeyi otomatik birleştirmesem satış gecikir mi?&lt;/strong&gt;&lt;br&gt;
Gecikme inceleme kuyruğunun tasarımına bağlıdır. Kuyruğu birinin hayır demesini bekleyen bir beklemeye dönüştürmeyin: süre dolunca geçici kişi üzerinden ilk temas kurulsun, bayrak kalsın, sonra insan karar versin. İlk temasın kaçmaması ile yanlış birleştirmenin olmaması aynı anda kurgulanabilir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;İki kişi birleştikten sonra sohbet geçmişi ne olur?&lt;/strong&gt;&lt;br&gt;
Kişi grafı doğru kurulmuşsa sohbet &lt;code&gt;person_id&lt;/code&gt; üzerinden okunur, lead olayları yerinde kalır. Yanlış birleştirmede &lt;code&gt;unlink&lt;/code&gt; sohbeti de ayırmalıdır; bu yüzden mesajları kişiye değil, mümkünse lead olayı veya konuşma oturumuna bağlamak daha güvenlidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hacim küçükseniz blocking key gerekir mi?&lt;/strong&gt;&lt;br&gt;
İlk yüz kayıtta fark etmez. İlk lansmanda binlerce form aynı güne yığılınca fark eder. Blocking key'i baştan koymak ucuzdur, sonradan tam taramayı temizlemek pahalıdır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yapay zekâ eşleştirmeyi devralabilir mi?&lt;/strong&gt;&lt;br&gt;
Adayları sıralayabilir, zayıf sinyalleri gerekçesiyle birlikte incelemeye bırakabilir. Otomatik &lt;code&gt;auto_link&lt;/code&gt; kararını kural tablosuna bırakın. Model bir gün değişir, kural tablosu ve olay defteri kalır; denetimde ikincisini göstermek daha kolaydır.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Koray Yalçın — lead yönetimi, CRM mimarisi, dijital pazarlama ve PropTech üzerine çalışıyor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>database</category>
      <category>crm</category>
      <category>backend</category>
    </item>
    <item>
      <title>CRM'de izin yönetimi tasarlamak: Değişmez rıza defteri, kanal ve amaç ayrımı, gönderim anında kontrol ve geri çekme</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Wed, 04 Nov 2026 08:17:59 +0000</pubDate>
      <link>https://dev.to/korayyalcin/crmde-izin-yonetimi-tasarlamak-degismez-riza-defteri-kanal-ve-amac-ayrimi-gonderim-aninda-fdm</link>
      <guid>https://dev.to/korayyalcin/crmde-izin-yonetimi-tasarlamak-degismez-riza-defteri-kanal-ve-amac-ayrimi-gonderim-aninda-fdm</guid>
      <description>&lt;p&gt;Bir gayrimenkul projesinin lansman haftasında pazarlama ekibi yeni bir SMS kampanyası hazırlıyor. Liste CRM'den çekiliyor, filtre "izinli = evet" olarak ayarlanıyor ve gönderim başlıyor. İki gün sonra bir müşteri adayı şikâyet ediyor: üç hafta önce "artık mesaj istemiyorum" diye yanıt vermiş, çağrı merkezi temsilcisi de bunu nota yazmış. Ama "izinli" alanı hiç değişmemiş, çünkü o alanı yalnız web formu dolduruyordu. Ben Koray Yalçın olarak lead yönetimi ve CRM kurulumlarında bu tabloyu defalarca gördüm: izin, bir onay kutusunun sonucu olarak tek bir boolean alanda tutulur ve sistemin geri kalanı o alanın doğru olduğunu varsayar.&lt;/p&gt;

&lt;p&gt;İzin yönetimi (consent management), bir kişinin hangi kanal üzerinden, hangi amaçla, ne zaman ve hangi metni görerek izin verdiğini ya da geri çektiğini kayıt altına alan ve bu kaydı her gönderimde uygulayan yapıdır. Bu yazıda kavramı bir geliştiricinin gözünden ele alıyorum: izni neden bir alan değil bir olay dizisi olarak tutmalıyız, kanal ve amaç ayrımını şemaya nasıl yansıtırız, kontrolü neden gönderimin son anında yaparız ve "beni listeden çıkarın" talebi tüm sistemlere nasıl yayılır.&lt;/p&gt;

&lt;p&gt;Bir not: Bu yazı teknik bir tasarım yazısıdır, hukuki görüş değildir. KVKK, Elektronik Ticaret Kanunu ve İleti Yönetim Sistemi (İYS) yükümlülüklerinin sizin işinize nasıl uygulandığını bir hukukçuyla netleştirin; aşağıdaki yapı o kararları güvenle uygulayabileceğiniz bir altyapı sunar.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzin neden bir boolean alan olamaz?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;marketing_opt_in = true&lt;/code&gt; alanı üç soruyu cevapsız bırakır. Birincisi, hangi kanal için? E-posta bülteni almak isteyen biri telefonla aranmak istemeyebilir. İkincisi, hangi amaç için? Satın aldığı daireyle ilgili teslim bilgilendirmesi ile yeni bir projenin kampanyası aynı şey değildir. Üçüncüsü, nasıl kanıtlanır? Bir şikâyet geldiğinde "evet, izin vardı" demek yetmez; ne zaman, hangi formda, hangi metin gösterilerek alındığını gösterebilmeniz gerekir.&lt;/p&gt;

&lt;p&gt;Bir de zaman boyutu var. İzin verilir, geri çekilir, yeniden verilir. Tek alanlı yapıda her değişiklik bir öncekini siler. Geçen ay bu kişiye gönderilen mesajın o tarihte izinli olup olmadığını sormak imkânsız hale gelir. Benim önerim şudur: izni bir &lt;strong&gt;olay defteri&lt;/strong&gt; olarak tut, güncel durumu bu defterden türet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Değişmez rıza defteri
&lt;/h2&gt;

&lt;p&gt;Defterin her satırı tek bir olaydır: verildi, geri çekildi ya da doğrulandı. Satırlar güncellenmez, silinmez; yalnız eklenir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;consent_channel&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'call'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'whatsapp'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;consent_action&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'granted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'withdrawn'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;consent_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;             &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;contact_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;        &lt;span class="n"&gt;consent_channel&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;purpose&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'marketing', 'project_updates', 'surveys'&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;         &lt;span class="n"&gt;consent_action&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;occurred_at&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- olayın gerçekte olduğu an&lt;/span&gt;
  &lt;span class="n"&gt;recorded_at&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;source&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'web_form', 'call_center', 'sms_reply', 'iys_sync'&lt;/span&gt;
  &lt;span class="n"&gt;notice_version&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c1"&gt;-- gösterilen metnin sürüm anahtarı&lt;/span&gt;
  &lt;span class="n"&gt;evidence&lt;/span&gt;       &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;actor_id&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- kaydı giren kullanıcı, otomatikse NULL&lt;/span&gt;
  &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;consent_event_lookup&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;consent_event&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;consent_event&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Birkaç ayrıntı önemli. &lt;code&gt;occurred_at&lt;/code&gt; ile &lt;code&gt;recorded_at&lt;/code&gt; ayrıdır, çünkü çağrı merkezinden gelen bir geri çekme talebi sisteme birkaç saat sonra girilebilir; hangi olayın önce geçerli olduğuna gerçek zamanla karar verilir. &lt;code&gt;idempotency_key&lt;/code&gt; aynı form gönderiminin ya da aynı İYS kaydının iki kez işlenmesini engeller. &lt;code&gt;REVOKE&lt;/code&gt; satırı uygulama kullanıcısının bu tabloyu değiştirmesini veritabanı düzeyinde yasaklar; yanlışlıkla yazılmış bir &lt;code&gt;UPDATE&lt;/code&gt; sorgusu sessizce geçmez, hata verir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kanıtı neyle saklarız?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;evidence&lt;/code&gt; alanı, izin alınan anın fotoğrafıdır. Web formu için tipik içerik şöyledir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"form_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proje-a-lansman-formu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://ornek-proje.com/basvuru"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notice_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"etk-2026-09"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notice_sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f2c...e1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkbox_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kampanya ve duyurular için SMS almak istiyorum."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prechecked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 ..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;notice_version&lt;/code&gt; ve &lt;code&gt;notice_sha256&lt;/code&gt; metnin kendisini değil, metnin hangi sürümünün gösterildiğini kaydeder. Metinlerin kendisi ayrı bir &lt;code&gt;consent_notice&lt;/code&gt; tablosunda sürümlü olarak durur. Böylece pazarlama ekibi onay kutusu yazısını değiştirdiğinde eski kayıtlar bozulmaz; her olay kendi metnine bağlı kalır. &lt;code&gt;prechecked: false&lt;/code&gt; küçük görünür ama önemlidir: önceden işaretlenmiş kutuyla alınmış bir onayın geçerliliği tartışmalıdır ve bunu sonradan kanıtlamak için o anda kaydetmek gerekir.&lt;/p&gt;

&lt;p&gt;IP ve tarayıcı bilgisi de kişisel veridir. Bunları saklama sürenizi veri saklama politikanızla uyumlu tutun ve bu alanları önceki yazımda anlattığım maskeleme kurallarına dahil edin; kanıt kaydı herkesin görebileceği bir ekran olmamalı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kanal ve amaç ayrımı
&lt;/h2&gt;

&lt;p&gt;Kanal (e-posta, SMS, arama, WhatsApp) ile amaç (pazarlama, proje bilgilendirmesi, anket) iki ayrı eksendir ve izin bu ikisinin kesişiminde verilir. Gayrimenkul tarafında en sık karışan ayrım şudur: satın alma süreci devam eden bir müşteriye ödeme hatırlatması ya da teslim tarihi bilgisi göndermek ile ona yeni bir projenin kampanyasını göndermek farklı hukuki dayanaklara ve farklı izin ihtiyacına sahip olabilir. Bu ayrımı şemada açıkça tutmazsanız, "işlemsel" etiketiyle kampanya gönderilmesi kaçınılmaz olur.&lt;/p&gt;

&lt;p&gt;Bunu kod düzeyinde zorlamanın yolu, her mesaj şablonunun bir amaca bağlı olmasıdır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;message_template&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;       &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;  &lt;span class="n"&gt;consent_channel&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;purpose&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;consent_purpose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;requires_consent&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;requires_consent = false&lt;/code&gt; olan amaçların listesi kısa olmalı ve değiştirilmesi bir kod incelemesi ya da yönetici onayı gerektirmelidir. Kampanya aracında kullanıcıya serbest metin alanı verip amacı seçtirmemek, en sık gördüğüm açıktır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Güncel durumu defterden türetmek
&lt;/h2&gt;

&lt;p&gt;Her gönderimde tüm defteri taramak pahalıdır. Güncel durumu ayrı bir tabloda tutarız, ama bu tablo yalnız defterden beslenir ve istenirse baştan yeniden hesaplanabilir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;consent_state&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;contact_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;      &lt;span class="n"&gt;consent_channel&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;purpose&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;       &lt;span class="n"&gt;consent_action&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;effective_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_event_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Olay eklendikten sonra aynı transaction içinde:&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;consent_state&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;effective_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;effective_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effective_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_event_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effective_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effective_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WHERE&lt;/code&gt; koşulu geç gelen eski bir olayın yeni durumu ezmesini önler. Saat dilimi ve sıra sorunlarında bir kural daha koyuyorum: aynı ana denk gelen bir "verildi" ile bir "geri çekildi" olayı varsa geri çekme kazanır. Belirsizlikte kısıtlayıcı taraf seçilir.&lt;/p&gt;

&lt;p&gt;Haftalık bir mutabakat işi, &lt;code&gt;consent_state&lt;/code&gt; tablosunu defterden sıfırdan hesaplayıp farkları raporlar. Fark çıkıyorsa bir yerde defteri atlayan bir yazma yolu vardır ve o yol bulunmalıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kontrol gönderimin son anında yapılır
&lt;/h2&gt;

&lt;p&gt;Kampanya listesini oluştururken izin filtrelemek gereklidir ama yeterli değildir. Liste pazartesi hazırlanıp çarşamba gönderiliyorsa aradaki iki günde gelen geri çekmeler kaçar. Bu yüzden izin kontrolünü mesajı sağlayıcıya teslim etmeden hemen önce, tek bir fonksiyonda yaparım:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConsentDenied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_can_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_consent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT status FROM consent_state
        WHERE contact_id = %s AND channel = %s AND purpose = %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;granted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ConsentDenied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outbox_item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outbox_item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;assert_can_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outbox_item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ConsentDenied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;mark_skipped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outbox_item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_consent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;provider_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outbox_item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Varsayılan "kayıt yoksa gönderme"dir. Kayıt yoksa izin de yoktur. Atlanan gönderimler sessizce kaybolmaz, &lt;code&gt;no_consent&lt;/code&gt; nedeniyle işaretlenir; böylece kampanya raporunda "1.200 kişilik listenin 140'ı gönderim anında izin dışı kaldı" gibi bir satır görürsünüz (sayılar örnek olarak verilmiştir). Bu satır, izin süreçlerinin işlediğinin en iyi kanıtıdır.&lt;/p&gt;

&lt;p&gt;Çağrı merkezi için de aynı fonksiyon kullanılır. Otomatik arayıcı (dialer) bir numarayı kuyruğa almadan önce &lt;code&gt;call/marketing&lt;/code&gt; iznini kontrol eder; manuel aramada ise temsilcinin ekranında arama düğmesi izin yoksa pasif görünür ve neden pasif olduğu yazar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geri çekmenin her yere yayılması
&lt;/h2&gt;

&lt;p&gt;Geri çekme talebi birçok yoldan gelir: e-postadaki abonelikten çık bağlantısı, SMS'e verilen "RET" yanıtı, WhatsApp'ta "mesaj istemiyorum" yazısı, çağrı merkezinde sözlü talep, İYS üzerinden yapılan ret. Hepsinin ortak noktası, sonunda aynı deftere bir &lt;code&gt;withdrawn&lt;/code&gt; olayı olarak düşmesidir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_withdrawal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;insert_consent_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;withdrawn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;upsert_consent_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;withdrawn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;enqueue_outbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consent.withdrawn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;purpose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outbox'a düşen olay, dış sistemlere yayılır: e-posta pazarlama aracındaki abonelik durumu, reklam platformlarına yüklenen özel kitleler, varsa İYS'ye bildirim ve iş ortaklarının listeleri. Reklam kitleleri en sık unutulan noktadır; izni geri çekilen bir kişinin telefon numarasının hash'li hali hâlâ bir yeniden hedefleme kitlesinde duruyor olabilir.&lt;/p&gt;

&lt;p&gt;Çağrı merkezindeki sözlü talep için temsilcinin serbest nota yazmasına izin vermiyorum. Görüşme sonucu kodlarına "pazarlama aramasını reddetti" gibi bir sonuç eklenir ve bu sonuç seçildiğinde &lt;code&gt;record_withdrawal&lt;/code&gt; doğrudan çağrılır. Notlar okunmaz; yapılandırılmış alanlar işlenir.&lt;/p&gt;

&lt;h2&gt;
  
  
  İleti Yönetim Sistemi ile eşitleme
&lt;/h2&gt;

&lt;p&gt;Türkiye'de ticari elektronik ileti izinleri İYS üzerinden de yönetilir ve alıcı iznini İYS'den de geri çekebilir. Bu, CRM'in tek doğruluk kaynağı olmadığı anlamına gelir. Benim tasarımımda İYS de bir olay kaynağıdır: oradan gelen değişiklikler &lt;code&gt;source = 'iys_sync'&lt;/code&gt; ile deftere eklenir, kendi &lt;code&gt;occurred_at&lt;/code&gt; değeriyle sıralanır ve aynı kurala tabi olur.&lt;/p&gt;

&lt;p&gt;Eşitlemede dikkat ettiğim iki konu var. Birincisi, CRM'de verilen bir iznin İYS'ye bildirilmesi başarısız olursa, bu durum bir hata kuyruğunda görünür olmalı ve izin "beklemede" sayılmalıdır. İkincisi, İYS'den gelen bir ret, CRM'deki daha eski bir onayı her zaman ezer; ama CRM'deki daha yeni bir onay İYS'ye bildirilmeden gönderimde kullanılmamalıdır. Bu ikinci kural için &lt;code&gt;consent_state&lt;/code&gt; tablosuna bir &lt;code&gt;synced_to_iys&lt;/code&gt; alanı eklemek ve gönderim kontrolünde bunu da sorgulamak yeterlidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yeniden izin isteme ve form tasarımı
&lt;/h2&gt;

&lt;p&gt;Geliştirici tarafında form da bu yapının parçasıdır. Her kanal ve amaç için ayrı, önceden işaretlenmemiş onay kutusu, aydınlatma metnine bağlantı ve gönderimde &lt;code&gt;notice_version&lt;/code&gt; değerinin sunucuya iletilmesi gerekir. İstemcinin gönderdiği sürüm anahtarı sunucuda doğrulanır; tanınmayan bir sürüm gelirse olay kaydedilmez, form hatası döner.&lt;/p&gt;

&lt;p&gt;Bir projeden diğerine geçişte de dikkat gerekir. A projesi için izin veren bir kişiye B projesinin kampanyasını göndermek, amaç tanımınız "firmanın tüm projeleri" değilse sorunludur. Bu yüzden &lt;code&gt;purpose&lt;/code&gt; anahtarlarını en baştan doğru kapsamla tanımlamak, sonradan veri taşımaktan çok daha ucuzdur.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzleme ve denetim
&lt;/h2&gt;

&lt;p&gt;Panoda üç sayıya bakıyorum. Gönderim anında izin nedeniyle atlanan mesaj oranı, geri çekme olayının deftere düşmesinden dış sistemlere yayılmasına kadar geçen süre ve haftalık mutabakatta çıkan fark sayısı. İlki sıfır olursa şüphelenirim, çünkü gerçek bir listede her zaman birkaç kişi arada fikrini değiştirir. İkincisi saatlerle ölçülüyorsa kuyrukta bir tıkanıklık vardır. Üçüncüsü sıfırdan büyükse defteri atlayan bir kod yolu vardır.&lt;/p&gt;

&lt;p&gt;Denetim için de defter yeterlidir: "Bu kişiye 12 Ekim'de gönderilen SMS için o anda geçerli izin var mıydı?" sorusu, &lt;code&gt;occurred_at &amp;lt;= '2026-10-12'&lt;/code&gt; koşuluyla en son olayı bulan tek bir sorguyla yanıtlanır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık sorulan sorular
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Eski sistemden gelen, kanıtı olmayan izinleri ne yapmalıyım?&lt;/strong&gt;&lt;br&gt;
Bunları deftere &lt;code&gt;source = 'legacy_import'&lt;/code&gt; ve boş kanıtla aktarın, ama gönderim kontrolünde ayrı bir sınıf olarak ele alın. Hangi gönderimlerde kullanılabileceğine hukuk ekibiyle birlikte karar verin; çoğu durumda yeniden izin isteme kampanyası en güvenli yoldur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geri çekme olayını silmek gerekirse ne olur?&lt;/strong&gt;&lt;br&gt;
Kişinin verilerinin silinmesi talebinde bile "bu kişiye mesaj gönderme" bilgisini tutmak gerekebilir; aksi halde kişi bir sonraki veri aktarımında yeniden listeye girer. Bunun için kimliği hash'lenmiş bir bastırma listesi (suppression list) tutulur. Hangi verinin ne kadar saklanacağına hukukçunuzla karar verin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;İşlemsel mesajlar için de defter gerekir mi?&lt;/strong&gt;&lt;br&gt;
İzin gerektirmeyen amaçlar için bile gönderim kaydı ve amacı tutulmalıdır. Bir mesajın gerçekten işlemsel olduğunu sonradan gösterebilmek, şablonun hangi amaca bağlı olduğunu kaydetmekle mümkündür.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Küçük bir emlak ofisi için bu yapı fazla değil mi?&lt;/strong&gt;&lt;br&gt;
Defter tablosu, durum tablosu ve tek bir &lt;code&gt;assert_can_send&lt;/code&gt; fonksiyonu birkaç günlük iştir ve en büyük riski kapatır. İYS eşitlemesi, reklam kitlesi temizliği ve mutabakat işi ekip ve hacim büyüdükçe eklenebilir. Önemli olan, ilk günden izni bir alan yerine olay olarak tutmaktır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WhatsApp için ayrı bir izin gerekir mi?&lt;/strong&gt;&lt;br&gt;
Kanal olarak ayrı tutmanızı öneririm. WhatsApp Business tarafında platformun kendi kuralları da vardır; bir kişinin SMS iznini WhatsApp için geçerli saymak hem platform hem mevzuat açısından risklidir.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Koray Yalçın — lead yönetimi, CRM mimarisi, dijital pazarlama ve PropTech üzerine çalışıyor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>database</category>
      <category>backend</category>
      <category>crm</category>
    </item>
    <item>
      <title>CRM'de veri maskeleme tasarlamak: Rol bazlı görünüm, sunucu tarafı maskeleme, açma kaydı ve test ortamı için tokenizasyon</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Tue, 03 Nov 2026 16:22:58 +0000</pubDate>
      <link>https://dev.to/korayyalcin/crmde-veri-maskeleme-tasarlamak-rol-bazli-gorunum-sunucu-tarafi-maskeleme-acma-kaydi-ve-test-34ab</link>
      <guid>https://dev.to/korayyalcin/crmde-veri-maskeleme-tasarlamak-rol-bazli-gorunum-sunucu-tarafi-maskeleme-acma-kaydi-ve-test-34ab</guid>
      <description>&lt;p&gt;Bir gayrimenkul satış ekibinde CRM ekranını açan herkes aynı şeyi görmemeli. Çağrı merkezindeki yeni başlayan bir temsilci, lead'i aramak için telefon numarasına ihtiyaç duyar ama o numarayı listede toplu halde görmesine gerek yoktur. Pazarlama analisti kampanya performansına bakarken kimin telefonu olduğunu hiç bilmemelidir. Bir entegrasyon ortağının destek ekibi hata ayıklarken gerçek e-posta adreslerini görmemelidir. Ben Koray Yalçın olarak lead yönetimi ve CRM kurulumlarında bu ayrımı en sık atlanan güvenlik katmanı olarak görüyorum: herkes "yetki" konuşur, ama yetkili olan kişinin bile neyi, ne kadarını ve ne zaman göreceği konuşulmaz.&lt;/p&gt;

&lt;p&gt;Veri maskeleme, kişisel verinin yetkisiz ya da gereğinden fazla göz önüne çıkmasını engellemek için değerin bir kısmının gizlenerek gösterilmesidir. En bilinen örneği &lt;code&gt;0532***1234&lt;/code&gt; biçiminde gösterilen telefon numarasıdır. Bu yazıda kavramı sözlük tanımından çıkarıp bir geliştiricinin gözünden ele alıyorum: maskelemeyi nerede yapacağız, kurallarını nasıl veri olarak tutacağız, "numarayı göster" düğmesinin arkasında ne olmalı, dışa aktarma ve loglar nasıl korunacak, test ortamına gerçek veri nasıl sızmayacak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maskeleme, şifreleme ve yetkilendirme aynı şey değil
&lt;/h2&gt;

&lt;p&gt;Bu üç kavram sık karıştırılır, oysa farklı soruları yanıtlarlar.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yetkilendirme&lt;/strong&gt; "bu kullanıcı bu kayda erişebilir mi?" sorusunu yanıtlar. Erişemiyorsa kayıt hiç gelmez.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Şifreleme&lt;/strong&gt; "veri diskte ya da ağda çalınırsa okunabilir mi?" sorusunu yanıtlar. Uygulama içinde yetkili kullanıcıya çözülmüş haliyle gelir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maskeleme&lt;/strong&gt; "erişimi olan kullanıcı alanın tamamını görmeli mi?" sorusunu yanıtlar. Kayda erişim vardır, ama belirli alanlar kısmen gizlenir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yani maskeleme, yetkilendirmenin alan düzeyindeki ince ayarıdır. Bir temsilci kendisine atanmış lead'e erişebilir (yetkilendirme), telefon sütunu veritabanında şifreli durabilir (şifreleme), ama liste ekranında numara maskeli görünür ve yalnız arama anında tam haliyle açılır (maskeleme). Üçü birbirinin yerine geçmez; birlikte çalışır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kural 1: Maskeleme sunucuda yapılır, arayüzde değil
&lt;/h2&gt;

&lt;p&gt;En yaygın hata, tam veriyi API'den gönderip arayüzde CSS ya da JavaScript ile gizlemektir. Tarayıcının geliştirici araçlarını açan herkes ağ sekmesinde tam numarayı görür. Mobil uygulamada da aynı şey geçerlidir: yanıt gövdesi cihazda önbelleğe alınır.&lt;/p&gt;

&lt;p&gt;Doğru yer, verinin sunucudan çıktığı son katmandır: serileştirme (serializer) ya da yanıt dönüştürücü. Böylece hangi uç noktadan gelirse gelsin aynı kural uygulanır. Kuralı her endpoint'e tek tek yazmak yerine tek bir fonksiyona bağlamak, yeni bir endpoint eklendiğinde maskelemenin unutulmasını engeller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Tek giriş noktası: her yanıt bu fonksiyondan geçer
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serialize_lead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Viewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FIELD_RULES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;          &lt;span class="c1"&gt;# kişisel veri olmayan alan
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada &lt;code&gt;context&lt;/code&gt; parametresi önemlidir: aynı kullanıcı liste ekranında, kayıt detayında, dışa aktarmada ve raporda farklı görünürlük seviyesine sahip olabilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kural 2: Kuralları kodda değil veride tut
&lt;/h2&gt;

&lt;p&gt;Hangi alanın kimde nasıl görüneceği zamanla değişir. Yeni bir rol eklenir, bir alan kişisel veri olarak işaretlenir, hukuk ekibi e-postanın da maskelenmesini ister. Bunu her seferinde kod değişikliğiyle yapmak hem yavaştır hem de denetlenmesi zordur. Kuralları bir tabloda tutmak, kimin neyi ne zaman değiştirdiğini de kayıt altına almayı kolaylaştırır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;field_mask_rule&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'lead', 'contact', 'deal'&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'phone', 'email', 'national_id'&lt;/span&gt;
  &lt;span class="n"&gt;mask_kind&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'phone', 'email', 'full', 'none'&lt;/span&gt;
  &lt;span class="k"&gt;role&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'agent', 'analyst', 'manager', 'partner'&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'list', 'detail', 'export', 'report', 'log'&lt;/span&gt;
  &lt;span class="k"&gt;level&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hidden'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'masked'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'revealable'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'clear'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;updated_by&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dört seviye pratikte yeterli oluyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;hidden&lt;/strong&gt;: Alan yanıtta hiç yer almaz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;masked&lt;/strong&gt;: Alan maskeli gelir, açılamaz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;revealable&lt;/strong&gt;: Alan maskeli gelir, kullanıcı gerekçeyle açabilir; açma kaydedilir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clear&lt;/strong&gt;: Alan tam gelir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Varsayılan değer her zaman en kısıtlayıcı olandır. Tabloda kuralı olmayan ama kişisel veri olarak işaretlenmiş bir alan &lt;code&gt;hidden&lt;/code&gt; sayılmalıdır. Böylece yeni eklenen bir alan için kural yazmayı unutmak veri sızıntısına değil, eksik görünüme yol açar. Eksik görünüm şikayet getirir ve düzeltilir; sızıntı ise çoğu zaman fark edilmez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maskeleme biçimleri: ne kadarını göstermeli?
&lt;/h2&gt;

&lt;p&gt;Maskelemenin amacı veriyi tamamen yok etmek değil, işi yapmaya yetecek kadarını bırakmaktır. Temsilci "bu, dün aradığım numara mı?" diye ayırt edebilmeli; analist aynı kişinin iki kez form doldurup doldurmadığını anlayabilmelidir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mask_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mask_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;***@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clear&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mask_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mask_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Birkaç pratik not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telefonda ülke kodu ve operatör önekini bırakmak çoğu zaman yeterli bağlam verir; son dört hane ayırt etmeyi sağlar. Kısa numaralarda daha az hane gösterin, aksi halde maskeleme anlamsızlaşır.&lt;/li&gt;
&lt;li&gt;E-postada alan adını açık bırakmak, kurumsal lead'leri ayırt etmeye yarar. Ama küçük bir şirketin alan adı tek başına kişiyi belli edebilir; hassas projelerde alan adını da maskeleyin.&lt;/li&gt;
&lt;li&gt;Kimlik numarası, pasaport numarası gibi alanlar için kısmi gösterim yerine &lt;code&gt;full&lt;/code&gt; maskeyi ve &lt;code&gt;revealable&lt;/code&gt; seviyesini tercih edin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "Numarayı göster" düğmesi: açma kaydı ve gerekçe
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;revealable&lt;/code&gt; seviyesindeki bir alanın açılması, ayrı bir uç noktaya yapılan bilinçli bir istektir. Liste yanıtı hiçbir zaman tam değeri taşımaz; kullanıcı düğmeye bastığında tek bir kaydın tek bir alanı için istek gider, sunucu yetkiyi yeniden kontrol eder, açma olayını kaydeder ve değeri döner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;pii_reveal_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;viewer_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- 'call', 'whatsapp', 'appointment', 'other'&lt;/span&gt;
  &lt;span class="n"&gt;note&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ip&lt;/span&gt;          &lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;revealed_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pii_reveal_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revealed_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gerekçe alanı ilk başta bürokrasi gibi görünür, ama iki işe yarar. Birincisi, kullanıcıyı bir an durdurur; "neden bu numaraya ihtiyacım var?" sorusunu sordurur. İkincisi, denetimde anlamlı bir iz bırakır. Arama gerekçesiyle açılan bir numaranın ardından gerçekten bir arama kaydı (call log) oluşmuş mu, bunu karşılaştırmak mümkün hale gelir.&lt;/p&gt;

&lt;p&gt;Daha da iyisi, birçok durumda numarayı hiç açmadan işi yaptırmaktır. Temsilci "Ara" düğmesine bastığında santral ya da tıkla-ara entegrasyonu numarayı sunucu tarafında çevirir; temsilci numarayı hiç görmez. WhatsApp mesajı da aynı şekilde şablon üzerinden sunucudan gönderilebilir. Benim kurduğum lead yönetimi akışlarında hedef, temsilcinin günlük işinin büyük kısmını tam numarayı görmeden tamamlayabilmesidir. Açma düğmesi istisna için kalmalıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anormal açma davranışını yakalamak
&lt;/h2&gt;

&lt;p&gt;Açma kaydı tutulduktan sonra basit kurallarla ciddi riskleri yakalamak mümkündür:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Son bir saatte olağan dışı sayıda açma yapan kullanıcılar&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;viewer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;reveals&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pii_reveal_log&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;revealed_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;viewer_id&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eşik değer ekibin gerçek iş yüküne göre belirlenmelidir; buradaki 40 yalnızca örnek bir değerdir. Eşiği aşan kullanıcı için açma uç noktası geçici olarak yavaşlatılabilir, yöneticiye bildirim gidebilir. İşten ayrılmadan önceki günlerde toplu numara açma, bu tablonun en sık ortaya çıkardığı örüntülerden biridir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dışa aktarma: en büyük sızıntı kapısı
&lt;/h2&gt;

&lt;p&gt;Ekranda maskeleme ne kadar iyi olursa olsun, "Excel'e aktar" düğmesi tüm emeği boşa çıkarabilir. Dışa aktarma ayrı bir &lt;code&gt;context&lt;/code&gt; olarak ele alınmalı ve kuralları ayrıca tanımlanmalıdır. Benim önerim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dışa aktarmada &lt;code&gt;revealable&lt;/code&gt; seviyesi yoktur; ya &lt;code&gt;masked&lt;/code&gt; ya &lt;code&gt;clear&lt;/code&gt;. Toplu açma, tek tek açmanın kaydını anlamsız kılar.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clear&lt;/code&gt; dışa aktarma yalnız belirli rollere açıktır ve her dışa aktarma işi kimin, hangi filtreyle, kaç satır aldığıyla birlikte kaydedilir.&lt;/li&gt;
&lt;li&gt;Dosya kalıcı bir bağlantıyla değil, kısa süreli imzalı bir bağlantıyla indirilir.&lt;/li&gt;
&lt;li&gt;Analiz amaçlı dışa aktarmalarda telefon ve e-posta yerine sabit bir takma kimlik (pseudonym) verilir. Analist aynı kişiyi tekrar eden satırlarda tanıyabilir ama kim olduğunu bilmez.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pseudonym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sırrı (secret) dışa aktarma işine özel ya da döneme özel tutmak, iki farklı dosyanın birleştirilerek kişilerin eşleştirilmesini zorlaştırır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loglar ve hata izleme araçları
&lt;/h2&gt;

&lt;p&gt;Uygulama logları, hata izleme servisleri ve kuyruk mesajları kişisel verinin en sessiz biriktiği yerlerdir. Bir hata yığını (stack trace) tüm istek gövdesini içerebilir; bir webhook hatası gelen lead'in telefonunu loglara yazabilir. Burada &lt;code&gt;context = 'log'&lt;/code&gt; için kural genellikle &lt;code&gt;masked&lt;/code&gt; ya da &lt;code&gt;hidden&lt;/code&gt; olmalıdır ve log kütüphanesine bir filtre eklenmelidir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;SENSITIVE_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;national_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passport_no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PiiFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;***&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SENSITIVE_KEYS&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anahtar adına göre filtrelemek tek başına yetmez; serbest metin içinde geçen telefon ve e-posta kalıplarını düzenli ifadeyle temizleyen ikinci bir katman da eklemek gerekir. Hata izleme servislerinin çoğu gönderim öncesi veri temizleme kancası sunar; bu kancayı aynı kural kümesine bağlamak tutarlılığı korur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test ve geliştirme ortamı: gerçek veri olmadan gerçekçi veri
&lt;/h2&gt;

&lt;p&gt;Üretim veritabanının bir kopyasını test ortamına almak hızlıdır ama riskli. Test ortamları genellikle daha zayıf korunur, daha çok kişi erişir ve üçüncü taraf araçlara bağlanır. Burada ekranda maskeleme yeterli değildir; verinin kendisi dönüştürülmelidir. Buna statik maskeleme ya da tokenizasyon denir.&lt;/p&gt;

&lt;p&gt;İyi bir test verisi dönüşümünün üç özelliği olmalı:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tutarlılık&lt;/strong&gt;: Aynı gerçek telefon her tabloda aynı sahte telefona dönüşmeli; böylece tekilleştirme ve eşleştirme mantığı testte de çalışır.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Biçim koruma&lt;/strong&gt;: Sahte telefon geçerli bir telefon biçiminde olmalı, sahte e-posta geçerli bir e-posta olmalı; doğrulama kuralları kırılmamalı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geri döndürülemezlik&lt;/strong&gt;: Test ortamına erişen biri sahte değerden gerçeğine ulaşamamalı.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fake_phone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pseudonym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0599&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;zfill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# kullanılmayan bir önek seçin
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fake_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;pseudonym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@example.test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sahte numaralar için gerçek bir operatöre ait olmayan bir önek seçmek, testten yanlışlıkla gerçek bir kişiye SMS ya da arama gitmesini engeller. Aynı şekilde sahte e-postalar için &lt;code&gt;example.test&lt;/code&gt; gibi teslim edilemeyen bir alan adı kullanılmalıdır. Dönüşüm sırrı test ortamında hiç bulunmamalıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  KVKK ve GDPR açısından nereye oturur?
&lt;/h2&gt;

&lt;p&gt;Maskeleme, kişisel verinin işlenme amacıyla sınırlı ve ölçülü tutulması ilkesinin teknik karşılıklarından biridir. KVKK ve GDPR, kişisel verilere erişimin işin gerektirdiği ölçüyle sınırlanmasını ve uygun teknik tedbirlerin alınmasını bekler. Maskeleme tek başına uyumluluk sağlamaz; açık rıza, aydınlatma metni, saklama süresi ve silme süreçleri ayrıca kurulmalıdır. Ama bir veri sızıntısı soruşturmasında "liste ekranında numaralar maskeliydi, açmalar gerekçeyle kaydediliyordu, dışa aktarma rol bazlıydı" diyebilmek, alınan tedbirin somut kanıtıdır. Hukuki yorum için her zaman bir uzmana danışılmalıdır; burada anlattıklarım mühendislik pratiğidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uygulamaya alırken kontrol listesi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kişisel veri alanlarını bir kayıt defterinde işaretleyin; işaretli ama kuralı olmayan alan varsayılan olarak gizli olsun.&lt;/li&gt;
&lt;li&gt;Maskelemeyi tek bir serileştirme katmanına bağlayın; arayüze tam veri göndermeyin.&lt;/li&gt;
&lt;li&gt;Liste, detay, dışa aktarma, rapor ve log için ayrı bağlam kuralları tanımlayın.&lt;/li&gt;
&lt;li&gt;Açma işlemini ayrı uç noktaya taşıyın; gerekçe ve kayıt zorunlu olsun.&lt;/li&gt;
&lt;li&gt;Tıkla-ara ve şablon mesaj ile numarayı göstermeden iletişim kurulmasını sağlayın.&lt;/li&gt;
&lt;li&gt;Açma kayıtlarında anormal hacmi izleyin.&lt;/li&gt;
&lt;li&gt;Test ortamına yalnız dönüştürülmüş veri aktarın.&lt;/li&gt;
&lt;li&gt;Kuralları her çeyrek rol değişiklikleriyle birlikte gözden geçirin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SSS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Veri maskeleme ile anonimleştirme arasındaki fark nedir?&lt;/strong&gt;&lt;br&gt;
Maskeleme, yetkili bir kullanıcı için veriyi kısmen gizler; gerçek değer sistemde durur ve gerektiğinde açılabilir. Anonimleştirmede ise veri kişiyle bir daha ilişkilendirilemeyecek şekilde dönüştürülür. Test ortamı için yapılan geri döndürülemez dönüşüm anonimleştirmeye yakındır; ekran maskelemesi değildir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maskelemeyi veritabanı düzeyinde yapmak daha güvenli değil mi?&lt;/strong&gt;&lt;br&gt;
Bazı veritabanları dinamik maskeleme özelliği sunar ve ek bir savunma hattı olarak işe yarar. Ama bağlam (liste mi, dışa aktarma mı, log mu) bilgisi genellikle uygulama katmanındadır. Pratikte en sağlam yapı, uygulama katmanında bağlam duyarlı maskeleme ve veritabanında rol bazlı erişim kısıtlamasının birlikte kullanılmasıdır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temsilci numarayı görmeden nasıl arama yapar?&lt;/strong&gt;&lt;br&gt;
Tıkla-ara entegrasyonu ile CRM, numarayı sunucu tarafında santrale iletir; temsilcinin telefonu çalar ve görüşme bağlanır. Temsilci ekranda yalnız maskeli numarayı görür. WhatsApp ve SMS için de şablonlu gönderim aynı mantıkla çalışır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maskeli telefonla tekrar eden lead'ler nasıl tespit edilir?&lt;/strong&gt;&lt;br&gt;
Tekilleştirme mantığı sunucuda tam değer üzerinde çalışır; kullanıcıya yalnız sonuç gösterilir. Analiz tarafında ise sabit takma kimlik kullanılır; aynı kişi farklı satırlarda aynı kimlikle görünür.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yönetici rolü her şeyi açık görmeli mi?&lt;/strong&gt;&lt;br&gt;
Hayır. Yönetici rolü genellikle rapor ve performans ekranlarında çalışır; orada tam numaraya ihtiyaç yoktur. Yöneticinin de &lt;code&gt;revealable&lt;/code&gt; seviyesinde olması ve açmalarının kaydedilmesi, hem kötüye kullanımı önler hem de ekibe doğru örnek olur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Küçük bir ekip için bu yapı fazla değil mi?&lt;/strong&gt;&lt;br&gt;
Dört kişilik bir ekipte bile liste ekranında maskeleme ve dışa aktarmanın kısıtlanması birkaç saatlik iştir ve en büyük riski kapatır. Açma kaydı, anormallik izleme ve test verisi dönüşümü ekip büyüdükçe eklenebilir. Önemli olan, varsayılanın en baştan kısıtlayıcı olmasıdır.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Koray Yalçın — lead yönetimi, CRM mimarisi, dijital pazarlama ve PropTech üzerine çalışıyor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>backend</category>
      <category>crm</category>
    </item>
    <item>
      <title>CRM ile dış sistemler arasında çift yönlü veri senkronizasyonu: Alan sahipliği, değişiklik imleci, döngü önleme ve çakışma çözümü</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Tue, 03 Nov 2026 08:26:48 +0000</pubDate>
      <link>https://dev.to/korayyalcin/crm-ile-dis-sistemler-arasinda-cift-yonlu-veri-senkronizasyonu-alan-sahipligi-degisiklik-imleci-3jdc</link>
      <guid>https://dev.to/korayyalcin/crm-ile-dis-sistemler-arasinda-cift-yonlu-veri-senkronizasyonu-alan-sahipligi-degisiklik-imleci-3jdc</guid>
      <description>&lt;p&gt;Bir CRM'i muhasebe yazılımına, proje stok tablosuna ya da pazarlama aracına bağlarken ilk istek hep aynı cümleyle gelir: "İki taraf da birbirinin aynısı olsun." Kâğıt üzerinde basit görünür. Pratikte ise çift yönlü senkronizasyon, entegrasyon işlerinin en çok sessiz hata üreten parçasıdır. Ben, Koray Yalçın, gayrimenkul satış ekipleri için lead yönetimi ve CRM altyapısı kurarken bu hataların neredeyse hepsini bir kez yaşadım: birbirini sonsuza kadar güncelleyen iki sistem, gece toplu güncellemeden sonra kaybolan telefon numaraları, saat farkı yüzünden "eski" sayılıp ezilen yeni kayıtlar.&lt;/p&gt;

&lt;p&gt;Bu yazıda tek yönlü "her gece dışa aktar" yaklaşımının ötesine geçip, iki sistemi güvenle eşitleyen bir senkronizasyon katmanını geliştirici gözüyle adım adım tasarlıyorum. Örnekler PostgreSQL ve TypeScript ile, ama fikirler hangi yığını kullanırsanız kullanın geçerli.&lt;/p&gt;

&lt;h2&gt;
  
  
  Önce soru: Gerçekten çift yönlü mü olmalı?
&lt;/h2&gt;

&lt;p&gt;En ucuz senkronizasyon, hiç çift yönlü olmayanıdır. Her alan için tek bir "sahip" sistem belirleyebiliyorsanız, sorunun büyük kısmı ortadan kalkar. Bir konut projesi örneği düşünelim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lead'in adı, telefonu, kaynağı ve danışman ataması CRM'de doğar ve CRM'e aittir.&lt;/li&gt;
&lt;li&gt;Ünitenin fiyatı ve durumu stok sisteminde doğar ve oraya aittir.&lt;/li&gt;
&lt;li&gt;Fatura numarası ve tahsilat durumu muhasebe yazılımına aittir.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bu durumda "çift yönlü" aslında "her alan tek yönlü, ama iki sistem de bazı alanların sahibi" demektir. Gerçek çakışma ancak iki sistemin de aynı alanı yazmasına izin verdiğinizde ortaya çıkar. O yüzden tasarıma kod yazarak değil, bir alan sahipliği tablosu çıkararak başlıyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alan sahipliğini veri olarak tutmak
&lt;/h2&gt;

&lt;p&gt;Sahiplik kuralını koda gömmek yerine bir tabloda tutmak, ileride "bu alanı artık muhasebe yönetsin" dendiğinde dağıtım yapmadan değişiklik yapmanızı sağlar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sync_field_policy&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;connector&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'accounting', 'inventory' ...&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'contact', 'deal', 'unit'&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;owner&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;owner&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'crm'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'remote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'both'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;conflict_rule&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'owner_wins'&lt;/span&gt;
    &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conflict_rule&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'owner_wins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'latest_wins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'manual'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;sync_field_policy&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'accounting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'full_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'crm'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'owner_wins'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'accounting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'tax_number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'remote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'owner_wins'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'accounting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'both'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'latest_wins'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'accounting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'phone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'both'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'manual'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;both&lt;/code&gt; olarak işaretlenen her alan bir uyarı işaretidir. Benim kuralım şu: bir alan &lt;code&gt;both&lt;/code&gt; ise, bunun neden gerekli olduğu tabloya yorum olarak yazılmadan canlıya çıkmaz. Telefon gibi kritik bir alanda &lt;code&gt;manual&lt;/code&gt; seçmek, yanlış numaraya otomatik mesaj gitmesinden çok daha ucuzdur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kimlik eşleme: İki sistemin aynı kaydı bilmesi
&lt;/h2&gt;

&lt;p&gt;Senkronizasyonun ikinci temeli, "CRM'deki 4812 numaralı kişi, muhasebedeki C-00931'dir" bilgisini güvenilir biçimde saklamaktır. Bu bilgiyi kayıtların kendisine bir &lt;code&gt;external_id&lt;/code&gt; sütunu olarak eklemek ilk bakışta pratiktir, ama birden fazla bağlayıcı geldiğinde dağılır. Ayrı bir eşleme tablosu daha temizdir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sync_link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;connector&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;local_id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;remote_id&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_local_hash&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- son gönderdiğimiz içeriğin özeti&lt;/span&gt;
  &lt;span class="n"&gt;last_remote_hash&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- son aldığımız içeriğin özeti&lt;/span&gt;
  &lt;span class="n"&gt;last_synced_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remote_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;İki benzersizlik kısıtı birlikte bire bir eşlemeyi garanti eder. Bir kayıt iki kez oluşturulmaya çalışılırsa veritabanı reddeder; böylece "aynı müşteri muhasebede iki kez açıldı" hatası uygulama mantığına güvenmeden engellenir. Hash alanlarını birazdan döngü önlemede kullanacağız.&lt;/p&gt;

&lt;h2&gt;
  
  
  Değişiklikleri yakalamak: Zaman damgası değil, imleç
&lt;/h2&gt;

&lt;p&gt;En yaygın hata, "son senkrondan sonra &lt;code&gt;updated_at&lt;/code&gt; değeri büyük olan kayıtları al" sorgusudur. İki sorunu var. Birincisi, aynı milisaniyede güncellenen kayıtlar sınırda kaybolabilir. İkincisi, uzun süren bir işlem eski bir zaman damgasıyla commit edildiğinde, siz o anı çoktan geçmiş olursunuz ve kayıt hiç görülmez.&lt;/p&gt;

&lt;p&gt;CRM tarafında daha sağlam yol, her değişikliğe artan bir sıra numarası veren bir değişiklik günlüğüdür (outbox). Kayıt güncellenirken aynı işlem içinde bu tabloya da bir satır yazılır:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;change_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;seq&lt;/span&gt;        &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;local_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;changed_fields&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;origin&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- 'user', 'workflow', 'sync:accounting'&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sync_cursor&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;connector&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_seq&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gönderici işçi &lt;code&gt;seq &amp;gt; last_seq&lt;/code&gt; olan satırları sırayla okur, işler ve imleci yalnızca başarılı işlemden sonra ilerletir. &lt;code&gt;bigserial&lt;/code&gt; değerlerinin commit sırasına göre değil tahsis sırasına göre arttığını unutmayın; çok yoğun sistemlerde kısa bir güvenlik gecikmesi bırakmak (örneğin son 5 saniyenin satırlarını bir sonraki tura bırakmak) boşluk kaçırma riskini azaltır.&lt;/p&gt;

&lt;p&gt;Uzak sistem tarafında ise elinizde ne varsa onu kullanırsınız: webhook, "şu tarihten beri değişenler" uç noktası ya da en kötü ihtimalle tam liste. Uzak sistem yalnızca zaman damgası veriyorsa, imleci "son görülen zaman eksi birkaç dakika" olarak tutup örtüşen kayıtları hash karşılaştırmasıyla elemek pratik bir çözümdür.&lt;/p&gt;

&lt;h2&gt;
  
  
  Döngüyü kırmak: Kendi yankınızı tanıyın
&lt;/h2&gt;

&lt;p&gt;Çift yönlü senkronizasyonun klasik kâbusu şudur: CRM bir alanı günceller, değişikliği muhasebeye gönderir; muhasebe bunu "yeni değişiklik" olarak webhook ile geri yollar; CRM bunu tekrar kaydeder ve yeni bir &lt;code&gt;change_log&lt;/code&gt; satırı oluşur. Döngü başlar.&lt;/p&gt;

&lt;p&gt;Bunu kırmanın iki katmanı var. İlki köken etiketidir: senkron işçisi CRM'e yazarken &lt;code&gt;origin = 'sync:accounting'&lt;/code&gt; kullanır ve muhasebeye giden işçi bu kökenli satırları atlar. İkincisi içerik özetidir: gönderdiğimiz alanların normalize edilmiş bir özetini saklarız; uzaktan gelen değişikliğin özeti bizim gönderdiğimizle aynıysa, bu bir yankıdır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRemoteChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SyncLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pickSyncedFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_local_hash&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_remote_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// kendi yazdığımızın yankısı ya da zaten gördüğümüz içerik&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Türkçe veride &lt;code&gt;normalize("NFC")&lt;/code&gt; küçük bir ayrıntı gibi görünse de önemlidir: bazı sistemler "ş" harfini birleşik karakter olarak, bazıları ayrık olarak gönderir. Normalize etmezseniz aynı isim iki farklı özet üretir ve döngü önleme sessizce çalışmaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Çakışma çözümü: "Son yazan kazanır" neden yetmez
&lt;/h2&gt;

&lt;p&gt;İki taraf aynı alanı iki senkron turu arasında değiştirdiğinde çakışma vardır. "Son yazan kazanır" kuralı basittir ama iki varsayıma dayanır: iki sistemin saatleri uyumludur ve zaman damgaları gerçekten değişikliğin zamanını gösterir. Muhasebe yazılımlarının çoğu toplu içe aktarmalarda tüm kayıtların &lt;code&gt;modified&lt;/code&gt; alanını aynı ana çeker; bu durumda yüzlerce kayıt, aslında eski olduğu halde "en yeni" görünür.&lt;/p&gt;

&lt;p&gt;Daha güvenilir yöntem üç yollu karşılaştırmadır. Son başarılı senkronda iki tarafın üzerinde anlaştığı değeri (taban) saklarsınız ve her alanda şunu sorarsınız: hangi taraf tabandan uzaklaştı?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Side&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;conflict_rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localChanged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;same&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remoteChanged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;same&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;localChanged&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;remoteChanged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localChanged&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;remoteChanged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;localChanged&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;remoteChanged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;same&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Gerçek çakışma: iki taraf da farklı yöne değiştirdi&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conflict_rule&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// latest_wins ayrıca ele alınır&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu yaklaşımda gerçek çakışmalar beklenenden çok daha az çıkar; çoğu "çakışma" aslında yalnızca bir tarafın değişmesidir. Gerçekten iki tarafın da değiştirdiği &lt;code&gt;manual&lt;/code&gt; alanlar ise bir inceleme kuyruğuna düşer ve CRM'de kaydın üzerinde görünür bir uyarı olarak durur. Satış operasyonunda bu kuyruk genelde günde birkaç kayıttan ibarettir; bunu bir insanın görmesi, yanlış telefon numarasıyla kaybedilen bir müşteriden çok daha ucuzdur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silme ve birleştirme: En çok unutulan iki olay
&lt;/h2&gt;

&lt;p&gt;Güncellemeleri senkronize etmek kolaydır; silmeler ve birleştirmeler tasarımı zorlar. Bir kayıt CRM'de silindiğinde muhasebede de silinmeli mi? Çoğu zaman hayır: faturası kesilmiş bir müşteri muhasebede kalmak zorundadır. Bu yüzden silmeyi ayrı bir politika olarak ele alıyorum: varsayılan davranış uzak kaydı silmek değil, eşlemeyi "bağlantısız" işaretleyip uzak tarafa bir arşiv etiketi göndermektir.&lt;/p&gt;

&lt;p&gt;Mükerrer lead birleştirme ise daha sinsidir. CRM'de iki kayıt birleştiğinde, kaybolan kaydın uzak sistemdeki karşılığı ne olacak? Eşleme tablosunda iki satır aynı hayatta kalan kayda işaret etmeye çalışır ve benzersizlik kısıtı haklı olarak itiraz eder. Çözüm, birleştirmeyi &lt;code&gt;change_log&lt;/code&gt; içinde ayrı bir olay türü olarak yaymak ve bağlayıcıya özel bir davranış tanımlamaktır: uzak sistem de birleştirmeyi destekliyorsa orada da birleştirmek, desteklemiyorsa ikinci kaydı arşivleyip eşlemesini hayatta kalan kayda not olarak eklemek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gözlemlenebilirlik: Senkron sessizce bozulur
&lt;/h2&gt;

&lt;p&gt;Senkronizasyon katmanı nadiren gürültülü biçimde çöker; genellikle sessizce geride kalır. Bu yüzden her bağlayıcı için şu üç ölçüyü panoda tutuyorum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gecikme:&lt;/strong&gt; &lt;code&gt;change_log&lt;/code&gt; içindeki en yüksek &lt;code&gt;seq&lt;/code&gt; ile bağlayıcının imleci arasındaki fark ve en eski işlenmemiş satırın yaşı.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Çakışma ve hata oranı:&lt;/strong&gt; Günlük çakışma kuyruğuna düşen kayıt sayısı ve kalıcı hataya düşen gönderimler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutabakat farkı:&lt;/strong&gt; Haftada bir, iki taraftaki kayıtların hash'lerini toplu olarak karşılaştıran bir mutabakat işi. Olay tabanlı senkron ne kadar iyi olursa olsun, arada kaçan bir webhook olur; mutabakat işi bu boşlukları kapatan güvenlik ağıdır.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mutabakat işinin bulduğu farkları otomatik düzeltmeden önce bir süre yalnızca raporlamasını öneririm. İlk haftalarda bulunan farklar genellikle bir normalize etme hatasını ya da yanlış işaretlenmiş bir alan sahipliğini ortaya çıkarır; bunları kuralda düzeltmek, sonuçları tek tek düzeltmekten çok daha kalıcıdır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canlıya alma sırası
&lt;/h2&gt;

&lt;p&gt;Yeni bir bağlayıcıyı canlıya alırken izlediğim sıra kabaca şöyle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Önce alan sahipliği tablosunu ilgili ekiplerle birlikte doldurun; &lt;code&gt;both&lt;/code&gt; alanları tek tek gerekçelendirin.&lt;/li&gt;
&lt;li&gt;Kimlik eşlemesini tek seferlik bir eşleştirme işiyle kurun (vergi numarası, e-posta, normalize telefon gibi güçlü anahtarlarla) ve emin olunamayan eşleşmeleri insan onayına bırakın.&lt;/li&gt;
&lt;li&gt;Senkronu bir süre "yalnızca okuma ve raporlama" kipinde çalıştırın; ne yazacağını kaydetsin ama yazmasın.&lt;/li&gt;
&lt;li&gt;Sonra tek yönlü alanları açın, en son &lt;code&gt;both&lt;/code&gt; alanları.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sonuç
&lt;/h2&gt;

&lt;p&gt;İyi bir veri senkronizasyonu, "iki sistem aynı olsun" isteğini birkaç açık karara böler: her alanın sahibi kim, kayıtlar birbirini nasıl tanıyor, değişiklik nasıl kaçırılmadan yakalanıyor, yankı nasıl tanınıyor ve gerçek çakışmada kim karar veriyor. Bu kararları veri olarak tuttuğunuzda, senkron katmanı bir kara kutu olmaktan çıkar ve satış ekibinin güvendiği bir altyapıya dönüşür. Kavramın kısa tanımı ve gayrimenkul satış operasyonlarındaki yeri için &lt;a href="https://www.korayyalcin.org/yayinlar-arastirmalar/sozluk/data-sync/" rel="noopener noreferrer"&gt;Data sync nedir?&lt;/a&gt; sayfasına göz atabilirsiniz.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Çift yönlü senkron için webhook mu, periyodik sorgu mu kullanmalıyım?&lt;/strong&gt;&lt;br&gt;
İkisini birlikte. Webhook düşük gecikme sağlar ama kaçabilir; periyodik "değişenleri getir" sorgusu ve haftalık mutabakat işi kaçanları yakalar. Yalnızca webhook'a güvenen bir senkron er geç sapar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;updated_at&lt;/code&gt; ile imleç tutmak neden riskli?&lt;/strong&gt;&lt;br&gt;
Aynı zaman damgasına sahip kayıtlar sınırda kaybolabilir ve uzun süren işlemler geçmiş bir zaman damgasıyla commit edilebilir. Artan sıra numaralı bir değişiklik günlüğü bu iki sorunu da ortadan kaldırır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sonsuz güncelleme döngüsünü en basit nasıl önlerim?&lt;/strong&gt;&lt;br&gt;
Senkron işçisinin yazdığı değişiklikleri bir köken etiketiyle işaretleyin ve o kökenli değişiklikleri karşı tarafa geri göndermeyin. Buna ek olarak, gönderdiğiniz içeriğin normalize edilmiş özetini saklayıp geri gelen değişikliği bu özetle karşılaştırın.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Son yazan kazanır" hiç mi kullanılmamalı?&lt;/strong&gt;&lt;br&gt;
Saatleri güvenilir, düşük riskli alanlarda (örneğin bir not alanı) kabul edilebilir. Telefon, e-posta, danışman ataması gibi satış akışını etkileyen alanlarda üç yollu karşılaştırma ve gerekirse insan onayı daha güvenlidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRM'de silinen bir lead uzak sistemde de silinmeli mi?&lt;/strong&gt;&lt;br&gt;
Genellikle hayır. Muhasebe veya sözleşme kayıtları yasal ve operasyonel nedenlerle kalmalıdır. Varsayılan davranış olarak eşlemeyi bağlantısız işaretleyip uzak kaydı arşivlemek daha güvenlidir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senkronun sağlıklı çalıştığını nasıl anlarım?&lt;/strong&gt;&lt;br&gt;
İmleç gecikmesini, çakışma kuyruğunun büyüklüğünü ve periyodik mutabakat farkını izleyin. Bu üç ölçü sıfıra yakın ve kararlıysa senkron sağlıklıdır; biri yavaşça artıyorsa sessiz bir sorun büyüyordur.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>database</category>
      <category>crm</category>
    </item>
    <item>
      <title>UTM parametrelerini CRM'e doğru taşımak: İlk ve son temas, normalize sözlük, kanal eşleme tablosu ve geriye dönük yeniden hesaplama</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Mon, 02 Nov 2026 16:25:10 +0000</pubDate>
      <link>https://dev.to/korayyalcin/utm-parametrelerini-crme-dogru-tasimak-ilk-ve-son-temas-normalize-sozluk-kanal-esleme-tablosu-2875</link>
      <guid>https://dev.to/korayyalcin/utm-parametrelerini-crme-dogru-tasimak-ilk-ve-son-temas-normalize-sozluk-kanal-esleme-tablosu-2875</guid>
      <description>&lt;p&gt;Pazarlama toplantılarında en sık duyduğum cümlelerden biri şudur: "Reklam paneli 400 lead diyor, CRM 310 diyor, satış ekibi ise kampanyanın hiç satış getirmediğini söylüyor." Bu üç rakam genellikle aynı gerçeği farklı kayıplarla anlatır. Kayıpların büyük kısmı da reklamdaki tıklama ile CRM'deki fırsat kartı arasındaki o kısa yolda, yani UTM parametrelerinin taşınmasında yaşanır. Ben Koray Yalçın; lead yönetimi, CRM mimarisi, dijital pazarlama ve yapay zekâ destekli otomasyon üzerine, çoğunlukla gayrimenkul ve PropTech projelerinde çalışıyorum. Konut projelerinde kampanya başına bütçeler yüksek, satış döngüleri uzun olduğu için "bu satış hangi reklamdan geldi" sorusunun cevabı doğrudan bütçe kararına dönüşüyor.&lt;/p&gt;

&lt;p&gt;UTM mapping, en basit tanımıyla, reklam ve kampanya bağlantılarındaki UTM parametrelerinin CRM'deki lead ve fırsat kayıtlarına otomatik ve tutarlı biçimde yazılmasıdır. Bu yazıda konuyu pazarlama tarafından değil, geliştirici tarafından ele alıyorum: parametreleri sayfaya ilk girişte nasıl yakalayacağınız, form ve oturum boyunca nasıl koruyacağınız, ham değeri ve normalize değeri neden ayrı tutmanız gerektiği, kanal eşlemesini neden kod yerine veri olarak saklamanız gerektiği ve eşleme kuralları değiştiğinde geçmişi nasıl yeniden hesaplayacağınız. Örnekler JavaScript ve PostgreSQL üzerinden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorun nerede başlıyor: UTM bir kez görünür, sonra kaybolur
&lt;/h2&gt;

&lt;p&gt;UTM parametreleri (&lt;code&gt;utm_source&lt;/code&gt;, &lt;code&gt;utm_medium&lt;/code&gt;, &lt;code&gt;utm_campaign&lt;/code&gt;, &lt;code&gt;utm_content&lt;/code&gt;, &lt;code&gt;utm_term&lt;/code&gt;) yalnızca kullanıcının geldiği ilk URL'de bulunur. Kullanıcı sitede ikinci sayfaya geçtiğinde, galeriye baktığında ya da form sayfasına gittiğinde adres çubuğunda artık yoktur. Formu gönderen sayfa çoğu zaman UTM'yi hiç görmemiştir. Bu yüzden "form gönderilirken URL'deki parametreleri okuyalım" yaklaşımı, tek sayfalık kampanya açılış sayfaları dışında neredeyse her zaman veri kaybettirir.&lt;/p&gt;

&lt;p&gt;İkinci sorun, aynı ziyaretçinin birden fazla kez gelmesidir. Konut alıcısı bir hafta önce Instagram reklamından gelip bakar, üç gün sonra Google'da proje adını arayıp tekrar gelir, sonunda bir e-posta bülteninden gelip formu doldurur. Hangi kaynağı yazacağınız bir tercih meselesidir ve bu tercihi kodun içine gömülü bir varsayım olarak değil, açık bir veri modeli olarak yapmanız gerekir.&lt;/p&gt;

&lt;p&gt;Üçüncü sorun ise dağınıklıktır. Aynı kaynak için &lt;code&gt;facebook&lt;/code&gt;, &lt;code&gt;Facebook&lt;/code&gt;, &lt;code&gt;fb&lt;/code&gt;, &lt;code&gt;meta&lt;/code&gt;, &lt;code&gt;ig&lt;/code&gt; ve &lt;code&gt;instagram&lt;/code&gt; değerlerini aynı hafta içinde görmek olağandır. Ajans değiştiğinde ya da bir ekip arkadaşı kampanya bağlantısını elle yazdığında yeni varyantlar eklenir. Normalize edilmemiş UTM verisiyle kurulan rapor, birkaç ay içinde güvenilmez hale gelir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yakalama katmanı: İlk temas ve son temas ayrı saklanır
&lt;/h2&gt;

&lt;p&gt;Benim önerdiğim temel kural şudur: tarayıcı tarafında hem ilk temas hem son temas bilgisi tutulur ve form gönderiminde ikisi birden sunucuya iletilir. İlk temas, ziyaretçinin bu cihazda UTM ile geldiği ilk ziyarettir ve bir kez yazıldıktan sonra değişmez. Son temas, UTM taşıyan en son ziyarettir ve her yeni kampanyalı girişte güncellenir. UTM'siz bir dönüş (doğrudan giriş ya da organik arama) son temas bilgisini silmez; aksi halde kullanıcı yer imine kaydettiği sayfadan döndüğü anda tüm kampanya bilgisi kaybolur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_campaign&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_term&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CLICK_IDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gclid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fbclid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wbraid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gbraid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ttclid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kt_attr_v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readTouch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;CLICK_IDS&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;landing_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referrer_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referrer&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referrer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;captureAttribution&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_DAYS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;864&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readTouch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;touch_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;touch_count&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Burada birkaç tasarım kararı var. Değerleri 200 karakterle kısaltıyorum, çünkü bazen reklam platformları ya da hatalı şablonlar çok uzun değerler üretir ve bunlar hem depolamayı hem raporları bozar. Tıklama kimliklerini (&lt;code&gt;gclid&lt;/code&gt;, &lt;code&gt;fbclid&lt;/code&gt; gibi) UTM ile aynı yerde ama ayrı alanlarda tutuyorum; çünkü daha sonra kapanan satışı reklam platformuna geri bildirmek istediğinizde UTM değil tıklama kimliği işe yarar. Süre sınırı koyuyorum, çünkü bir yıl önceki bir reklam tıklamasını bugünkü forma bağlamak çoğu işletme için anlamlı değildir. Doksan gün bir örnek değerdir; kendi satış döngünüze göre seçmelisiniz.&lt;/p&gt;

&lt;p&gt;Çerez onayı konusunu da baştan çözmek gerekir. Bu bilgileri yerel depolamaya yazmak, kullandığınız onay yönetimi yapısına ve hukuki değerlendirmenize göre onay gerektirebilir. Pratikte iki yol görüyorum: onay verilene kadar parametreleri yalnızca oturum boyunca bellekte tutup form gönderiminde iletmek ya da onay sonrasında kalıcı depolamaya yazmak. Hangisini seçerseniz seçin, CRM'e gelen kayıtta "atıf verisi hangi onay durumunda toplandı" bilgisini de saklamanızı öneririm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form ve sunucu tarafı: Tarayıcıya güvenmeyin, ama ondan gelen veriyi kaybetmeyin
&lt;/h2&gt;

&lt;p&gt;Form gönderiminde atıf durumu gizli alanlar ya da JSON gövdesinin bir parçası olarak sunucuya gider. Sunucu bu veriyi doğrulamadan CRM'e yazmamalıdır; çünkü sonuçta kullanıcının değiştirebileceği bir veridir. Doğrulamanın amacı sahteciliği tamamen engellemek değil, bozuk ve tehlikeli değerleri ayıklamaktır: izin verilen anahtarlar dışındakileri atmak, uzunluğu sınırlamak, kontrol karakterlerini temizlemek ve zaman damgasının makul bir aralıkta olduğunu kontrol etmek.&lt;/p&gt;

&lt;p&gt;Sunucu tarafında ikinci bir güvence katmanı da kurulabilir. Açılış sayfası isteği sunucunuza geldiğinde, URL'deki UTM'leri birinci taraf bir çerezle eşleşen bir ziyaret kaydına yazarsanız, tarayıcı tarafındaki kod bir reklam engelleyiciye takıldığında bile atıf verisini kurtarabilirsiniz. Form gönderiminde tarayıcıdan gelen değer ile sunucudaki ziyaret kaydı karşılaştırılır; ikisi çelişirse sunucu kaydı tercih edilir ve çelişki not edilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Veri modeli: Ham değer, normalize değer ve temas tablosu
&lt;/h2&gt;

&lt;p&gt;En sık gördüğüm hata, UTM değerlerini doğrudan lead kartındaki birkaç metin alanına yazmaktır. Bu, hem tek temas varsayımını kalıcı hale getirir hem de normalizasyon kuralları değiştiğinde geçmişi düzeltmeyi imkânsızlaştırır. Benim tercih ettiğim yapı, her teması ayrı bir satır olarak saklamak ve normalize sonucu ayrı alanlarda tutmaktır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;attribution_touch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lead_id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;touch_role&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touch_role&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'first'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'last'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'conversion'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;occurred_at&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;-- ham değerler: asla değiştirilmez&lt;/span&gt;
  &lt;span class="n"&gt;raw_source&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_medium&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_campaign&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_content&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_term&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;click_id_type&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;click_id&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;landing_path&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;referrer_host&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;consent_state&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;-- normalize sonuç: yeniden hesaplanabilir&lt;/span&gt;
  &lt;span class="n"&gt;source_norm&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;medium_norm&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;campaign_key&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mapping_version&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;touch_role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ham alanlar bir kez yazılır ve hiç güncellenmez. Normalize alanlar ise hangi eşleme sürümüyle üretildiklerini &lt;code&gt;mapping_version&lt;/code&gt; üzerinden belli eder. Bu ayrım, yazının ilerleyen bölümündeki geriye dönük yeniden hesaplamayı mümkün kılan şeydir. &lt;code&gt;conversion&lt;/code&gt; rolü, formun gönderildiği oturumu temsil eder; ilk ve son temas ile aynı olabilir ama ayrı tutmak, "form gönderen ziyaret organikti ama kullanıcıyı ilk getiren reklamdı" gibi durumları görünür kılar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize sözlük ve kanal eşleme tablosu: Kural veri olarak
&lt;/h2&gt;

&lt;p&gt;Normalizasyonu iki aşamada yapıyorum. Birinci aşama mekanik temizliktir: küçük harfe çevirme, baştaki ve sondaki boşlukları silme, boşluk ve tireleri alt çizgiye dönüştürme. İkinci aşama anlamsal eşlemedir: &lt;code&gt;fb&lt;/code&gt;, &lt;code&gt;facebook&lt;/code&gt;, &lt;code&gt;meta&lt;/code&gt; ve &lt;code&gt;ig&lt;/code&gt; değerlerinin hangi kanonik kaynağa karşılık geldiğine karar vermek. Bu ikinci aşamayı kodun içinde &lt;code&gt;if&lt;/code&gt; zinciriyle yapmak, pazarlama ekibinin her yeni varyant için geliştiriciye iş açmasına yol açar. Bu yüzden eşlemeyi bir tabloya koyuyorum.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;utm_alias&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'source'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'medium'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;raw_clean&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;canonical&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_clean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;channel_rule&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="nb"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;priority&lt;/span&gt;     &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source_norm&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- NULL: herhangi biri&lt;/span&gt;
  &lt;span class="n"&gt;medium_norm&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- NULL: herhangi biri&lt;/span&gt;
  &lt;span class="n"&gt;has_click_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- 'gclid', 'fbclid' ya da NULL&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'paid_social', 'paid_search', 'email', 'referral' ...&lt;/span&gt;
  &lt;span class="k"&gt;version&lt;/span&gt;      &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;channel_rule&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kanal kuralları öncelik sırasına göre değerlendirilir ve ilk eşleşen kural kazanır. Tıklama kimliğini bir koşul olarak eklemek, UTM'si eksik ama &lt;code&gt;gclid&lt;/code&gt; taşıyan ziyaretleri ücretli arama olarak sınıflandırmanızı sağlar; bu durum Google Ads'in otomatik etiketlemesi açıkken ama manuel UTM eklenmemişken sık görülür.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[\s\-]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^a-z0-9_\.]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw_source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;med&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw_medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;med&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unmapped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# priority sıralı gelir
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_norm&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_norm&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;medium_norm&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;medium_norm&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_click_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_click_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;click_id_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unmapped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;referral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;referrer_host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_norm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium_norm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unmapped&lt;/code&gt; değerini bilerek ayrı tutuyorum. Eşleşmeyen bir değeri "diğer" kategorisine gömmek, sorunu gizler. &lt;code&gt;unmapped&lt;/code&gt; oranı ise pazarlama ekibi için çok değerli bir sinyaldir: yeni bir ajans ya da yeni bir kampanya şablonu devreye girdiğinde bu oran yükselir ve sözlüğe eklenmesi gereken değerler hemen görünür.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kampanya anahtarı: İsimlendirme kuralı olmadan eşleme yarım kalır
&lt;/h2&gt;

&lt;p&gt;Kaynak ve ortam normalize edilse bile &lt;code&gt;utm_campaign&lt;/code&gt; alanı genellikle en dağınık olanıdır. Konut projelerinde benim önerdiğim yaklaşım, kampanya adına sabit bir yapı vermektir: proje kodu, hedef kitle ya da pazar, teklif türü ve dönem. Örneğin &lt;code&gt;prj12_uk_lansman_2026q4&lt;/code&gt; gibi bir değer, alt çizgiyle ayrılmış dört parçaya bölünebilir. Bu yapıya uyan değerlerden bir &lt;code&gt;campaign_key&lt;/code&gt; türetilir ve fırsat kartında projeyle ilişkilendirilir. Uymayan değerler yine ham haliyle saklanır, ama raporda "isimlendirme dışı" olarak işaretlenir.&lt;/p&gt;

&lt;p&gt;Bu kuralı bağlantı üretim aşamasında uygulamak, sonradan temizlemekten çok daha ucuzdur. Pazarlama ekibine serbest metin alanı yerine, açılır listelerden kampanya bağlantısı üreten küçük bir iç araç vermek, birkaç hafta içinde veri kalitesini belirgin biçimde değiştirir. Bu aracın ürettiği değerler doğrudan &lt;code&gt;utm_alias&lt;/code&gt; ve proje tablosu ile uyumlu olur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lead'den fırsata: Atıf kopyalanmaz, bağlanır
&lt;/h2&gt;

&lt;p&gt;Lead bir fırsata dönüştüğünde UTM alanlarının fırsat kartına kopyalanması yaygın bir uygulamadır. Kopyalama, iki kayıt arasında zamanla ayrışan iki gerçek yaratır. Ben fırsat kaydında atıf verisini kopyalamak yerine, kaynak lead'in temas satırlarına bağlanmayı tercih ediyorum. Raporlama tarafında bir görünüm, fırsatı ilk ve son temasla birleştirir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;deal_attribution&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deal_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_gbp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaign_key&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_campaign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;campaign_key&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_campaign&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deal&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt; &lt;span class="n"&gt;ld&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_lead_id&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;attribution_touch&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touch_role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'first'&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;attribution_touch&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touch_role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'last'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bir kişinin birden fazla lead kaydı olduğunda (farklı formlar, farklı projeler) hangi lead'in fırsatın kaynağı olduğu, tekilleştirme ve birleştirme kurallarıyla belirlenir. Birleştirme sırasında temas satırları silinmemeli, birleşen kayda taşınmalıdır; böylece kişinin gerçek ilk teması korunur.&lt;/p&gt;

&lt;h2&gt;
  
  
  UTM'siz kanallar: Telefon, WhatsApp ve satış ofisi
&lt;/h2&gt;

&lt;p&gt;Gayrimenkulde lead'lerin önemli bir kısmı forma hiç uğramaz. Kullanıcı reklamı görür, numarayı arar ya da WhatsApp'tan yazar, bazen de doğrudan satış ofisine gelir. Bu kanallarda UTM yoktur, ama atıf ihtiyacı devam eder. Uyguladığım üç yöntem şunlardır. Birincisi, kampanya bazlı takip numaraları kullanmak ve gelen aramanın numarasını bir kampanya anahtarına eşlemektir. İkincisi, WhatsApp'a yönlendiren bağlantılara önceden doldurulmuş bir mesaj içinde kısa bir referans kodu eklemek ve gelen ilk mesajda bu kodu ayrıştırmaktır. Üçüncüsü, satış ofisine gelen ziyaretçiye danışmanın "bizi nereden duydunuz" sorusunu sabit seçeneklerle sormasıdır. Bu üçünün sonucu da &lt;code&gt;attribution_touch&lt;/code&gt; tablosuna yazılır; ham değer olarak takip numarası, referans kodu ya da beyan edilen seçenek saklanır ve aynı normalizasyon hattından geçer.&lt;/p&gt;

&lt;p&gt;Beyan edilen kaynağın dijital atıfla çeliştiği durumlar sık görülür. Bunları birbirini ezmesine izin vermeden iki ayrı temas olarak saklamak, hem pazarlama hem satış ekibine daha dürüst bir tablo sunar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geriye dönük yeniden hesaplama: Eşleme değiştiğinde geçmiş de değişir
&lt;/h2&gt;

&lt;p&gt;Sözlüğe yeni bir takma ad eklendiğinde ya da bir kanal kuralı değiştiğinde, geçmiş kayıtların normalize alanları eski kurallarla kalır. Ham değerleri sakladığınız için bu sorun çözülebilir: yeni kural sürümüyle tüm temasları yeniden hesaplarsınız. Bunu büyük tablolarda tek bir dev güncellemeyle değil, parçalar halinde yapmak gerekir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reproject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aliases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_aliases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_rules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;last_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;SELECT id, raw_source, raw_medium, click_id_type, referrer_host
               FROM attribution_touch
               WHERE id &amp;gt; %s AND (mapping_version IS NULL OR mapping_version &amp;lt; %s)
               ORDER BY id LIMIT %s&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_asdict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;UPDATE attribution_touch
                   SET source_norm=%s, medium_norm=%s, channel=%s, mapping_version=%s
                   WHERE id=%s&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_norm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium_norm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;last_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yeniden hesaplama sonrası en önemli adım, değişen kayıtların özetini çıkarmaktır: kaç temasın kanalı değişti, hangi kanaldan hangisine geçti. Bu özet, "geçen ayın raporu neden değişti" sorusunun cevabıdır ve pazarlama ekibiyle güveni koruyan şey budur. Kapanmış dönem raporlarını dondurmak isteyen ekipler için rapor anlık görüntüsünü ayrıca saklamak da iyi bir pratiktir.&lt;/p&gt;

&lt;h2&gt;
  
  
  İzleme: Eşlemenin sağlığını haftalık ölçün
&lt;/h2&gt;

&lt;p&gt;Kurulumdan sonra izlenmesi gereken birkaç basit gösterge var. Atıf verisi hiç olmayan dijital lead oranı, yakalama katmanında bir kırılma olduğunu gösterir; örneğin bir site güncellemesi script'i devre dışı bırakmış olabilir. &lt;code&gt;unmapped&lt;/code&gt; kanal oranı sözlüğün güncel olup olmadığını gösterir. İsimlendirme dışı kampanya oranı, bağlantı üretim aracının kullanılıp kullanılmadığını gösterir. Reklam panelindeki dönüşüm sayısı ile CRM'deki aynı kampanyaya ait lead sayısı arasındaki fark ise tek başına kesin bir sonuç vermez ama ani sıçramalar çoğu zaman bir entegrasyon sorununa işaret eder.&lt;/p&gt;

&lt;p&gt;Bu göstergeleri bir panoya koymak yeterli değildir; bir eşik belirleyip aşıldığında ilgili kişiye bildirim gitmesini sağlamak gerekir. Atıf hatları sessizce bozulur ve fark edildiklerinde genellikle birkaç haftalık veri çoktan kaybolmuştur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık sorulan sorular
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;İlk temas mı son temas mı kullanmalıyım?&lt;/strong&gt;&lt;br&gt;
İkisini de saklayın, raporlama sırasında seçin. Lansman dönemindeki bilinirlik kampanyalarının etkisini görmek için ilk temas, kısa vadeli performans kampanyalarını optimize etmek için son temas daha anlamlıdır. Tek bir alan seçip diğerini atmak, sonradan geri dönülemeyen bir karardır.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UTM verisini sadece reklam platformunun panelinden takip etsem yetmez mi?&lt;/strong&gt;&lt;br&gt;
Panel, tıklama ve form doldurma düzeyinde bilgi verir. Hangi lead'in randevuya geldiği, hangisinin satışa dönüştüğü ve satış tutarı ise CRM'dedir. Bütçe kararını satış verisine göre vermek istiyorsanız atıfın CRM'de doğru yaşaması gerekir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ham değerleri saklamak veri tabanını şişirmez mi?&lt;/strong&gt;&lt;br&gt;
Temas başına birkaç kısa metin alanı, lead kaydının geri kalanıyla karşılaştırıldığında küçük bir yüktür. Karşılığında kural değiştiğinde geçmişi düzeltme imkânı kazanırsınız. Ham değer olmadan bu düzeltme mümkün değildir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reklam engelleyiciler UTM yakalamayı bozar mı?&lt;/strong&gt;&lt;br&gt;
Kendi alan adınızdan sunulan birinci taraf script'ler genellikle daha az etkilenir, ama risk tamamen ortadan kalkmaz. Açılış sayfası isteğini sunucu tarafında kaydetmek, tarayıcı tarafı başarısız olduğunda bir yedek sağlar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tıklama kimliklerini (gclid, fbclid) neden ayrıca saklamalıyım?&lt;/strong&gt;&lt;br&gt;
UTM size kampanyayı söyler, tıklama kimliği ise reklam platformunun kendi kaydına bağlanmanızı sağlar. Kapanan satışları reklam platformuna offline dönüşüm olarak geri bildirmek istediğinizde ihtiyaç duyacağınız bilgi budur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kanal eşleme kurallarını kim yönetmeli?&lt;/strong&gt;&lt;br&gt;
Kuralların kendisi pazarlama operasyonunun sorumluluğunda olmalı, ama değişiklikler sürümlü ve kayıtlı yapılmalı. Geliştirici, sürüm geçişinde yeniden hesaplamanın çalışmasını ve değişiklik özetinin üretilmesini garanti eder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapanış
&lt;/h2&gt;

&lt;p&gt;UTM mapping, ilk bakışta birkaç gizli form alanından ibaret görünür. Gerçekte ise yakalama, saklama, normalizasyon, eşleme ve yeniden hesaplamadan oluşan küçük bir veri hattıdır. İlk ve son teması ayrı tutmak, ham değeri hiç değiştirmemek, eşlemeyi kod yerine sürümlü veri olarak saklamak, UTM'siz kanalları aynı hatta dahil etmek ve hattın sağlığını düzenli ölçmek, "reklam ne kadar satış getirdi" sorusuna savunulabilir bir cevap vermenin temelidir. Benim deneyimimde bütçe toplantılarındaki tartışmaların önemli bir kısmı, rakamların kendisinden değil, bu hattın hiç kurulmamış olmasından kaynaklanır.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Koray Yalçın — lead yönetimi, CRM mimarisi ve PropTech sistemleri&lt;/em&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>postgres</category>
      <category>javascript</category>
      <category>crm</category>
    </item>
    <item>
      <title>CRM'den dışarıya webhook yayınlamak: Abonelik kaydı, imzalı gövde, teslimat kuyruğu ve endpoint sağlığı</title>
      <dc:creator>Koray Yalçın</dc:creator>
      <pubDate>Mon, 02 Nov 2026 08:22:34 +0000</pubDate>
      <link>https://dev.to/korayyalcin/crmden-disariya-webhook-yayinlamak-abonelik-kaydi-imzali-govde-teslimat-kuyrugu-ve-endpoint-3d6h</link>
      <guid>https://dev.to/korayyalcin/crmden-disariya-webhook-yayinlamak-abonelik-kaydi-imzali-govde-teslimat-kuyrugu-ve-endpoint-3d6h</guid>
      <description>&lt;p&gt;Bir CRM'i başka sistemlere bağlarken genellikle ilk düşünülen yön içeri doğrudur: reklam platformundan gelen lead'i almak, form verisini kaydetmek, ödeme sağlayıcısından gelen bildirimi işlemek. Oysa bir süre sonra soru tersine döner. Satış ekibi "lead kazanıldı olarak işaretlendiğinde muhasebe sistemi haberdar olsun" der, pazarlama ekibi "randevu oluştuğunda reklam aracına bildirim gitsin" ister, bir iş ortağı ise "yeni ilan eklendiğinde bize anında haber verin" diye sorar. Ben Koray Yalçın; lead yönetimi, CRM mimarisi, dijital pazarlama ve yapay zekâ destekli otomasyon üzerine, ağırlıklı olarak gayrimenkul ve PropTech projelerinde çalışıyorum. Bu taleplerin hepsinin ortak cevabı giden webhook'tur ve gördüğüm entegrasyon sorunlarının önemli bir kısmı, webhook'un "olay olunca bir POST atarız" kadar basit sanılmasından çıkar.&lt;/p&gt;

&lt;p&gt;Bu yazıda giden webhook'u (outbound webhook) bir geliştirici gözüyle ele alıyorum: abonelik kaydını nasıl tutacağınız, olayı işlemle aynı anda nasıl güvenle kaydedeceğiniz, gövdeyi nasıl imzalayacağınız, teslimatı nasıl kuyruğa alacağınız, sorunlu bir endpoint'i nasıl yöneteceğiniz ve alıcı tarafında neyi garanti edip neyi etmediğinizi nasıl açıkça söyleyeceğiniz. Örnekler PostgreSQL ve Python üzerinden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook bir bildirim değil, bir sözleşmedir
&lt;/h2&gt;

&lt;p&gt;Webhook'u bir kez yayınladığınızda karşı tarafta biri ona göre kod yazar. Alan adını değiştirdiğinizde, bir olayı iki kez gönderdiğinizde ya da sırayı karıştırdığınızda, sorunu siz değil o kod yaşar. Bu yüzden ilk adım teknik değil, sözleşmeseldir. Hangi olay türlerini yayınladığınızı, her olayın gövdesinde hangi alanların bulunduğunu, teslimatın "en az bir kez" mi olduğunu, sıranın garanti edilip edilmediğini ve imzanın nasıl doğrulanacağını yazılı olarak belirlemeniz gerekir.&lt;/p&gt;

&lt;p&gt;Benim önerdiğim temel sözleşme şudur: teslimat en az bir kezdir, yani aynı olay birden fazla gelebilir; her olayın değişmeyen bir kimliği vardır; sıra garanti edilmez ama her olay, ait olduğu kaydın sürüm numarasını taşır; gövde imzalıdır ve imza bir zaman damgası içerir. Bu dört cümleyi dokümantasyonun en üstüne yazmak, sonradan gelecek destek taleplerinin yarısını önler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abonelik kaydı: kim, neyi, nereye istiyor
&lt;/h2&gt;

&lt;p&gt;Webhook altyapısının merkezinde abonelik tablosu durur. Her abonelik bir hedef URL, bir olay türü listesi, bir imza sırrı ve bir durum taşır. Burada sık yapılan hata, sırrı düz metin olarak saklamak ya da tek bir sırrı tüm abonelikler için kullanmaktır. Her aboneliğin kendi sırrı olmalı, sır şifreli saklanmalı ve rotasyon için iki sır aynı anda geçerli olabilmelidir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_subscription&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;             &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;target_url&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_types&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;secret_enc&lt;/span&gt;     &lt;span class="n"&gt;bytea&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;secret_prev_enc&lt;/span&gt; &lt;span class="n"&gt;bytea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;secret_prev_expires_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
                 &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'paused'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'disabled'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;disabled_reason&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload_version&lt;/span&gt; &lt;span class="nb"&gt;smallint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;webhook_subscription&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_types&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;payload_version&lt;/code&gt; alanı göz ardı edilen ama çok değerli bir alandır. Gövde biçimini değiştirmeniz gerektiğinde, mevcut abonelere eski sürümü göndermeye devam edip yeni abonelikleri yeni sürümle başlatabilirsiniz. Böylece hiç kimsenin entegrasyonu bir gece ansızın bozulmaz.&lt;/p&gt;

&lt;p&gt;Hedef URL'yi kaydederken de dikkatli olmak gerekir. Kullanıcının girdiği bir URL'ye sunucunuzdan istek atmak, sunucu taraflı istek sahteciliği (SSRF) için açık bir kapıdır. Kayıt sırasında yalnızca &lt;code&gt;https&lt;/code&gt; şemasına izin vermek, gönderim anında ise alan adının çözüldüğü IP adresinin özel ağ aralıklarında (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, bulut metadata adresleri) olmadığını kontrol etmek gerekir. Kontrolü yalnızca kayıt anında yapmak yetmez; alan adı daha sonra başka bir adrese yönlendirilebilir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Olayı işlemle aynı anda kaydetmek: outbox
&lt;/h2&gt;

&lt;p&gt;Webhook'u doğrudan iş kodunun içinden göndermek cazip gelir: lead kazanıldı olarak güncellendi, hemen ardından HTTP isteği atıldı. Bu yaklaşımın iki ciddi sorunu var. Birincisi, veritabanı işlemi geri alınırsa webhook yine de gitmiş olur ve karşı taraf hiç gerçekleşmemiş bir olayı işler. İkincisi, karşı tarafın yavaşlığı kullanıcının ekranını yavaşlatır.&lt;/p&gt;

&lt;p&gt;Çözüm, olayı iş verisiyle aynı veritabanı işlemi içinde bir outbox tablosuna yazmaktır. Gönderim, ayrı bir işçi tarafından yapılır. İşlem başarıyla tamamlanmadıysa olay da yoktur; tamamlandıysa olay kesin olarak kayıtlıdır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_deal_won&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deal_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;UPDATE deal SET stage = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;won&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, version = version + 1,
                      updated_at = now()
               WHERE id = %s AND stage &amp;lt;&amp;gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;won&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
               RETURNING id, tenant_id, version, amount, currency&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deal_id&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;# zaten kazanılmış, olay üretme
&lt;/span&gt;        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO event_outbox
                 (event_id, tenant_id, event_type, entity_id,
                  entity_version, payload, occurred_at)
               VALUES (gen_random_uuid(), %s, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deal.won&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, %s, %s, %s, now())&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deal_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actor_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;})),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dikkat edilecek nokta, olayın &lt;code&gt;entity_version&lt;/code&gt; taşımasıdır. Alıcı taraf, elindeki sürümden daha eski bir olay gelirse onu yok sayabilir. Sıra garantisi vermeden tutarlılık sağlamanın en pratik yolu budur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Olaydan teslimata: yayılım (fan-out)
&lt;/h2&gt;

&lt;p&gt;Bir olay birden fazla aboneliğe gidebilir. Outbox'taki her olay için ilgili aktif abonelikler bulunur ve her biri için ayrı bir teslimat kaydı oluşturulur. Teslimatı abonelik bazında ayırmak önemlidir; çünkü bir endpoint'in çökmesi, diğer abonelerin olayları almasını engellememelidir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_delivery&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;webhook_subscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt;        &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
                  &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'succeeded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'dead'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;attempt_count&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;next_attempt_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;last_status_code&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;webhook_delivery&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_attempt_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;UNIQUE (subscription_id, event_id)&lt;/code&gt; kısıtı, yayılım işçisi iki kez çalışsa bile aynı olay için aynı aboneliğe iki teslimat kaydı açılmasını engeller. Gönderim işçisi ise &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; ile zamanı gelmiş kayıtları alır; birden fazla işçi aynı teslimatı aynı anda göndermez.&lt;/p&gt;

&lt;h2&gt;
  
  
  İmzalı gövde: alıcı neye güvenebilir
&lt;/h2&gt;

&lt;p&gt;Alıcının, gelen isteğin gerçekten sizden geldiğini ve yolda değiştirilmediğini doğrulayabilmesi gerekir. En yaygın ve sağlam yöntem, gövdeyi aboneliğe özel sırla HMAC-SHA256 ile imzalamaktır. İmzaya bir zaman damgası ve olay kimliği eklemek, eski bir isteğin yeniden oynatılmasını da zorlaştırır.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload_version&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Webhook-Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Webhook-Timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Webhook-Signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sır rotasyonu sırasında, eski sır da geçerliyken iki imza birden gönderilir (&lt;code&gt;v1=...,v1=...&lt;/code&gt;). Alıcı ikisinden biri tutarsa isteği kabul eder. Dokümantasyonda alıcıya şunu açıkça söylemek gerekir: imzayı ham gövde üzerinden doğrulayın, JSON'u ayrıştırıp yeniden oluşturduktan sonra değil; karşılaştırmayı sabit zamanlı yapın; zaman damgası beş dakikadan eskiyse reddedin; &lt;code&gt;Webhook-Id&lt;/code&gt; değerini kaydedip aynı kimlikle gelen ikinci isteği işlemeden 200 dönün.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teslimat işçisi: zaman aşımı, yanıt kodları ve yeniden deneme
&lt;/h2&gt;

&lt;p&gt;Gönderim işçisinin en önemli ayarı zaman aşımıdır. Alıcı on saniye içinde yanıt vermiyorsa beklemeyi bırakmak gerekir; aksi halde yavaş bir endpoint tüm işçileri kilitler. Alıcıya da "isteği aldığınızda hemen 2xx dönün, işi arka planda yapın" önerisi verilmelidir.&lt;/p&gt;

&lt;p&gt;Yanıt kodlarını sınıflandırmak gerekir. 2xx başarıdır. 410 Gone, alıcının bu aboneliği artık istemediği anlamına gelir; aboneliği devre dışı bırakmak doğrudur. 4xx kodlarının çoğu kalıcı bir sorun işaretidir ama yine de birkaç kez denemek, alıcının geçici bir yapılandırma hatasını düzeltmesine zaman tanır. 429 ve 5xx ile ağ hataları geçicidir; &lt;code&gt;Retry-After&lt;/code&gt; başlığı varsa ona uyulur, yoksa üstel bekleme ve rastgele sapma (jitter) ile tekrar denenir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SCHEDULE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;43200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# saniye
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;succeeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;disable_subscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endpoint 410 döndü&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCHEDULE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCHEDULE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SCHEDULE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;schedule_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bu çizelgeyle bir teslimat yaklaşık üç gün boyunca denenir. "Dead" durumuna düşen teslimatlar silinmez; abonelik sahibine bir panelden görünür kılınır ve tek tek ya da toplu olarak yeniden gönderilebilir. Yeniden gönderimde olay kimliği aynı kalır; böylece alıcının tekilleştirme mantığı bozulmaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint sağlığı: ne zaman durdurmalı
&lt;/h2&gt;

&lt;p&gt;Haftalardır yalnızca hata dönen bir endpoint'e her olayı göndermeye devam etmek hem kaynak israfıdır hem de kuyruğu şişirir. Benim uyguladığım kural basittir: bir abonelik son yetmiş iki saat boyunca hiç başarılı teslimat almadıysa ve bu sürede en az belli sayıda deneme yapıldıysa, otomatik olarak &lt;code&gt;disabled&lt;/code&gt; durumuna alınır, sahibine e-posta ile bildirilir ve panelde gerekçe gösterilir. Abonelik yeniden etkinleştirildiğinde, bekleyen teslimatlar sırayla tekrar denenir.&lt;/p&gt;

&lt;p&gt;Bir de devre kesici (circuit breaker) mantığı vardır. Bir endpoint arka arkaya hata dönüyorsa, o aboneliğin tüm bekleyen teslimatlarını aynı anda denemek yerine yalnızca bir "keşif" teslimatı gönderilir. O başarılı olursa diğerleri serbest bırakılır. Bu, kendini toparlamaya çalışan bir alıcı sistemin üzerine yüzlerce isteğin aynı anda yığılmasını önler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gövde tasarımı: ince mi, kalın mı
&lt;/h2&gt;

&lt;p&gt;Gövdeye kaydın tamamını koymak (kalın gövde) alıcının işini kolaylaştırır ama iki risk taşır: kişisel veriyi gereğinden fazla yaymak ve alıcının eski bir anlık görüntüye göre işlem yapması. İnce gövde ise yalnızca olay türünü, kayıt kimliğini ve sürümü taşır; alıcı güncel veriyi API'den çeker.&lt;/p&gt;

&lt;p&gt;Benim tercihim ortada bir yerdir. Olayı anlamak için gereken alanlar gövdede bulunur; telefon, e-posta ve kimlik bilgisi gibi kişisel veriler bulunmaz. Alıcı bunlara ihtiyaç duyarsa, kapsamı sınırlandırılmış bir API anahtarıyla çeker. Bu yaklaşım, KVKK açısından da veri minimizasyonu ilkesiyle uyumludur ve hangi verinin hangi sisteme gittiğini denetlemeyi kolaylaştırır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gözlemlenebilirlik ve test ortamı
&lt;/h2&gt;

&lt;p&gt;Webhook sorunlarının çoğu "bize hiç gelmedi" cümlesiyle başlar. Bu soruya kanıtla cevap verebilmek için her teslimat denemesinin zamanını, yanıt kodunu, süresini ve yanıt gövdesinin ilk birkaç yüz karakterini saklamak gerekir. Abonelik sahibine bu kayıtları gösteren bir panel, destek ekibinin yükünü belirgin biçimde azaltır.&lt;/p&gt;

&lt;p&gt;Ayrıca abonelik sahibine bir "test olayı gönder" düğmesi sunmak, entegrasyonun canlıya almadan doğrulanmasını sağlar. Test olayları gerçek olaylarla aynı imza ve biçimi kullanmalı, ancak türleri açıkça ayrılmalıdır (örneğin &lt;code&gt;test.ping&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sık Sorulan Sorular
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Webhook yerine alıcının düzenli olarak API'yi sorgulaması daha basit değil mi?&lt;/strong&gt;&lt;br&gt;
Düşük hacimde öyle görünebilir, ancak sorgulama ya gecikme yaratır ya da gereksiz yük üretir. Webhook ile sorgulamayı birleştirmek en sağlam yoldur: webhook anlık bildirim sağlar, alıcı da kaçırdığı olayları yakalamak için arada bir "şu tarihten sonraki olaylar" uç noktasını sorgular.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Olayların sırasını garanti edebilir miyim?&lt;/strong&gt;&lt;br&gt;
Abonelik başına tek işçi ve tek kuyrukla teorik olarak evet, ama bir teslimat başarısız olduğunda arkasındaki tüm olaylar bekler. Bunun yerine sıra garantisi vermeyip her olaya kayıt sürüm numarası eklemek çok daha pratiktir.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;İmza için HMAC yerine imzalı JWT ya da asimetrik anahtar kullanmalı mıyım?&lt;/strong&gt;&lt;br&gt;
Asimetrik imza, sırrı alıcıyla paylaşmama avantajı sağlar ve çok sayıda alıcı olduğunda yönetimi kolaylaştırır. Çoğu CRM entegrasyonunda ise abonelik başına HMAC sırrı yeterli, basit ve yaygın olarak desteklenen bir çözümdür.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alıcı aynı olayı iki kez işlerse ne olur?&lt;/strong&gt;&lt;br&gt;
Teslimat en az bir kez olduğu için bu ihtimal her zaman vardır. Alıcı &lt;code&gt;Webhook-Id&lt;/code&gt; değerini benzersiz kısıtlı bir tabloya yazıp, ekleme başarısız olursa işlemi atlamalıdır. Sözleşmede bunu açıkça belirtmek, sorumluluğu doğru yere koyar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook'larla kişisel veri göndermek KVKK açısından sorun yaratır mı?&lt;/strong&gt;&lt;br&gt;
Veri gönderilen her sistem, veri işleme envanterinde yer almalı ve aktarımın bir amacı olmalıdır. Gövdede kişisel veri taşımamak, gerektiğinde kapsamlı API erişimiyle çekmek ve aktarımları kayıt altına almak, riski önemli ölçüde azaltır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapanış
&lt;/h2&gt;

&lt;p&gt;Giden webhook, bir CRM'in dış dünyayla konuştuğu en görünür yüzeylerden biridir. Abonelik başına sır ve sürüm, işlemle aynı anda yazılan outbox, abonelik bazında ayrılmış teslimat kayıtları, zaman damgalı imza, sınıflandırılmış yeniden deneme, endpoint sağlığı ve şeffaf teslimat kayıtları; bunların her biri tek başına küçük bir karar gibi görünür. Birlikte ise "bize hiç gelmedi" ile "iki kez geldi" arasında gidip gelen destek konuşmalarını sona erdirir. Benim deneyimimde iş ortakları bir CRM'e, yayınladığı webhook'ların ne kadar öngörülebilir olduğuna bakarak güvenir.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Koray Yalçın — lead yönetimi, CRM mimarisi ve PropTech sistemleri&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>api</category>
      <category>backend</category>
      <category>crm</category>
    </item>
  </channel>
</rss>
