From fa433920776363160f1620374ef74a5b97fb277d Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:47:24 +0300 Subject: [PATCH 1/4] =?UTF-8?q?fix(analitik):=20g=C3=B6r=C3=BCn=C3=BCm=20h?= =?UTF-8?q?ook'u=20callback=20ref=20+=20erken=20olay=20kuyru=C4=9Fu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useGorunumOlayi useRef + [ad, anahtarEki] deps'li effect kullanıyordu: eleman ilk commit'te yoksa (kaydet-banner: mount sonrası açılır, portal içinde) ya da sökülüp yeni düğümle geri geliyorsa (tadımlık satırı) gözlem hiç kurulmuyordu → kaydet_banner_goruntulendi / tadimlik_goruntulendi = 0. Hook artık callback ref döner; gözlem düğüm bağlanınca kurulur, sökülünce React 19 ref temizliğiyle bırakılır. Çağıranlar yalnız ref={…} verdiği için kullanım yerleri değişmedi. olay(): Rybbit script'i afterInteractive; sert yüklemede mount anında atılan olaylar window.rybbit henüz yokken sessizce düşüyordu. Artık en çok 20 olay, 500 ms aralıkla en çok 20 deneme (10 sn) kuyrukta bekler; script gelmezse (reklam engelleyici, dev) kuyruk atılır. olayBirKez(): hook'taki sessionStorage tekilleştirmesinin effect'ten çağrılabilir hâli (anahtar biçimi aynı: kolaytercih.olay.[.]). Co-Authored-By: Claude Fable 5.1 --- src/components/use-gorunum-olayi.ts | 66 ++++++++++---------- src/lib/analitik.ts | 93 +++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/src/components/use-gorunum-olayi.ts b/src/components/use-gorunum-olayi.ts index 53739f1..a1df9c4 100644 --- a/src/components/use-gorunum-olayi.ts +++ b/src/components/use-gorunum-olayi.ts @@ -1,54 +1,60 @@ "use client"; -import { useEffect, useRef, type RefObject } from "react"; -import { olay, type OlayAdi, type Ozellikler } from "@/lib/analitik"; +import { useCallback, useEffect, useRef, type RefCallback } from "react"; +import { + olayAnahtari, + olayBirKez, + type OlayAdi, + type Ozellikler, +} from "@/lib/analitik"; /** * Bir elemanın viewport'a ilk girişinde analitik olayı atan ref hook'u. * Gösterim olayları (hazir_panel_goruntulendi, kilit_goruntulendi vb.) * oturum başına en fazla 1 kez sayılmalı; tekilleştirme sessionStorage - * anahtarıyla yapılır. IntersectionObserver yoksa olay hiç atılmaz — - * "görüntülendi" ölçümünde görünürlüğü doğrulayamadığımız cihazı saymak - * veriyi şişirir. + * anahtarıyla yapılır (bkz. olayBirKez). IntersectionObserver yoksa olay hiç + * atılmaz — "görüntülendi" ölçümünde görünürlüğü doğrulayamadığımız cihazı + * saymak veriyi şişirir. + * + * Callback ref döner (RefObject değil): gözlem effect'te değil, DOM düğümü + * bağlandığı anda kurulur. Eleman ilk commit'te yoksa (kaydet-banner: mount + * sonrası açılır, üstelik portal içinde) ya da sökülüp yeni bir düğümle geri + * geliyorsa (tadımlık satırı: tip uymayınca gizlenip fetch sonrası yeniden + * takılır) useRef + effect deseni gözlemi hiç kurmuyordu. */ export function useGorunumOlayi( ad: OlayAdi, ozellikler?: Ozellikler, /** Aynı olayın farklı yüzeyleri için tekilleştirme eki ("satir", "kart"). */ anahtarEki?: string, -): RefObject { - const ref = useRef(null); - // Özellikler her render sonrası tazelenir; observer effect'i yalnızca - // ad/anahtar değişiminde kurulur (deps'e obje koymamak için ref). +): RefCallback { + // Özellikler her render sonrası tazelenir; callback ref yalnızca ad/anahtar + // değişiminde yeniden kurulur (deps'e obje koymamak için ref). const ozelliklerRef = useRef(ozellikler); useEffect(() => { ozelliklerRef.current = ozellikler; }); - useEffect(() => { - const el = ref.current; - if (!el || typeof IntersectionObserver === "undefined") return; + return useCallback( + (el: T | null) => { + if (!el || typeof IntersectionObserver === "undefined") return; - const anahtar = `kolaytercih.olay.${ad}${anahtarEki ? `.${anahtarEki}` : ""}`; - try { - if (window.sessionStorage.getItem(anahtar)) return; - } catch { - // Storage engelliyse tekilleştirmeden vazgeç, olay yine atılabilir - } - - const io = new IntersectionObserver((entries) => { - if (!entries.some((e) => e.isIntersecting)) return; - io.disconnect(); try { - window.sessionStorage.setItem(anahtar, "1"); + if (window.sessionStorage.getItem(olayAnahtari(ad, anahtarEki))) return; } catch { - // yut + // Storage engelliyse tekilleştirmeden vazgeç, olay yine atılabilir } - olay(ad, ozelliklerRef.current); - }); - io.observe(el); - return () => io.disconnect(); - }, [ad, anahtarEki]); - return ref; + const io = new IntersectionObserver((entries) => { + if (!entries.some((e) => e.isIntersecting)) return; + io.disconnect(); + olayBirKez(ad, ozelliklerRef.current, anahtarEki); + }); + io.observe(el); + // React 19 ref temizliği: düğüm sökülünce (ya da ad/anahtar değişince) + // gözlemci bırakılır; yeni düğüm bağlanınca callback baştan çalışır. + return () => io.disconnect(); + }, + [ad, anahtarEki], + ); } diff --git a/src/lib/analitik.ts b/src/lib/analitik.ts index 2ed81cb..c60c552 100644 --- a/src/lib/analitik.ts +++ b/src/lib/analitik.ts @@ -3,8 +3,9 @@ * * Script yalnızca prod build'de yükleniyor (bkz. app/layout.tsx) ve reklam * engelleyiciler tarafından bloklanabiliyor; bu yüzden buradaki her çağrı - * `window.rybbit` yoksa sessizce hiçbir şey yapmaz — çağıran taraf asla - * varlık kontrolü yapmak zorunda kalmasın. + * `window.rybbit` yoksa kullanıcıya hiçbir şey hissettirmez (olaylar kısa + * süre kuyrukta bekler, script gelmezse atılır) — çağıran taraf asla varlık + * kontrolü yapmak zorunda kalmasın. * * KURAL: Öğrencinin ham başarı sıralaması analitiğe GÖNDERİLMEZ. Sıralama * her zaman `siraKovasi()` ile bantlanır. @@ -48,16 +49,100 @@ declare global { } } -/** Özel olay gönderir. Script yoksa no-op. */ +// Erken olay kuyruğu: script `afterInteractive` yüklendiği için sert sayfa +// yüklemesinde (OAuth dönüşü, e-postadaki link, yer imi) mount effect'leri +// `window.rybbit`'ten önce koşar. Mount anında atılan olaylar +// (sonuc_goruntulendi, paket_goruntulendi) kaybolmasın diye script gelene +// kadar burada bekletilir. Script hiç gelmezse (reklam engelleyici, dev) +// kuyruk 10 sn sonra sessizce atılır ve bir daha doldurulmaz. +const KUYRUK_SINIRI = 20; +const DENEME_ARALIGI_MS = 500; +const DENEME_SINIRI = 20; + +let kuyruk: Array<[OlayAdi, Ozellikler | undefined]> = []; +let zamanlayici: ReturnType | null = null; +let denemeSayisi = 0; +let kuyrukKapali = false; + +function zamanlayiciyiDurdur(): void { + if (zamanlayici !== null) clearInterval(zamanlayici); + zamanlayici = null; +} + +/** Bekleyen olayları geliş sırasıyla gönderir. */ +function kuyruguBosalt(rybbit: RybbitApi): void { + const bekleyenler = kuyruk; + kuyruk = []; + zamanlayiciyiDurdur(); + for (const [ad, ozellikler] of bekleyenler) { + try { + rybbit.event(ad, ozellikler); + } catch { + // yut + } + } +} + +function kuyrugaEkle(ad: OlayAdi, ozellikler?: Ozellikler): void { + if (kuyrukKapali || kuyruk.length >= KUYRUK_SINIRI) return; + kuyruk.push([ad, ozellikler]); + if (zamanlayici !== null) return; + zamanlayici = setInterval(() => { + denemeSayisi += 1; + if (window.rybbit) { + kuyruguBosalt(window.rybbit); + } else if (denemeSayisi >= DENEME_SINIRI) { + // Script gelmedi: bekleyenleri at, sonraki olaylar eski no-op'a dönsün + kuyruk = []; + kuyrukKapali = true; + zamanlayiciyiDurdur(); + } + }, DENEME_ARALIGI_MS); +} + +/** Özel olay gönderir. Script henüz yoksa kısa süre kuyrukta bekletir. */ export function olay(ad: OlayAdi, ozellikler?: Ozellikler): void { if (typeof window === "undefined") return; try { - window.rybbit?.event(ad, ozellikler); + if (!window.rybbit) { + kuyrugaEkle(ad, ozellikler); + return; + } + // Sıra bozulmasın: bekleyen varsa önce onlar gider + if (kuyruk.length > 0) kuyruguBosalt(window.rybbit); + window.rybbit.event(ad, ozellikler); } catch { // Analitik hiçbir koşulda kullanıcı akışını bozmaz } } +/** Oturum başına tekilleştirme anahtarı (useGorunumOlayi ile ortak). */ +export function olayAnahtari(ad: OlayAdi, anahtarEki?: string): string { + return `kolaytercih.olay.${ad}${anahtarEki ? `.${anahtarEki}` : ""}`; +} + +/** + * Olayı oturum (sekme) başına en fazla 1 kez gönderir; "görüntülendi" türü + * olaylar yenileme ve geri-ileri gezinmede şişmesin diye. Tekilleştirme + * sessionStorage anahtarıyla yapılır. + */ +export function olayBirKez( + ad: OlayAdi, + ozellikler?: Ozellikler, + /** Aynı olayın farklı yüzeyleri/durumları için tekilleştirme eki. */ + anahtarEki?: string, +): void { + if (typeof window === "undefined") return; + const anahtar = olayAnahtari(ad, anahtarEki); + try { + if (window.sessionStorage.getItem(anahtar)) return; + window.sessionStorage.setItem(anahtar, "1"); + } catch { + // Storage engelliyse tekilleştirmeden vazgeç, olay yine atılır + } + olay(ad, ozellikler); +} + /** * Ham sıralamayı analitik için bantlar. Panelde "hangi sıralama bandı * dönüşüyor" sorusunu yanıtlar ama tekil öğrenciyi işaret etmez. From cf19903e19302ee0ea690b54fefc9d6de5fcc7cc Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:49:09 +0300 Subject: [PATCH 2/4] =?UTF-8?q?feat(analitik):=20huni=20olaylar=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Huninin kör noktalarına altı olay (adlar kalıcı sözleşme): - cta_form_goruntulendi: programatik sayfalardaki CTA kartı, yalnız profilsiz dalda (görünüm hook'u; sayfa ayrımı Rybbit pathname'inden) - sira_form_hata {kaynak: hero|cta|kapi|duzenle}: facet isteği hatası; geçersiz sayı doğrulaması sayılmaz - sonuc_goruntulendi {girisli, sira_kovasi}: /sonuc pathname'i maskeli, huni adımı bu olayla kurulur; oturum başına girişli/anonim birer kez - paket_goruntulendi {kaynak: kilit|kredi_bitti|nav|diger}: sayfaya dokunmadan yeni paket/layout.tsx + client bileşenle; /paket linklerine ?kaynak= eklendi (listem kilit kartları, kredi-yetersiz, nav kredi pill'i) - liste_uretim_hata {kod: HATA|ISTISNA}: AUTH/KREDI/PAKET dalları atmaz - giris_hata {yontem: eposta|google, status} Ham sıra ve e-posta hiçbir olaya girmez; sıra yalnız siraKovasi() ile. Co-Authored-By: Claude Fable 5.1 --- src/app/paket/layout.tsx | 16 ++++++++++++ .../kullanici/components/giris-form.tsx | 2 ++ .../kullanici/components/user-nav.tsx | 2 +- .../components/tercih-profili-kapisi.tsx | 1 + .../odeme/components/paket-gorunum-olayi.tsx | 25 +++++++++++++++++++ .../rapor/components/liste-uretici.tsx | 4 ++- .../rapor/components/listem-govde.tsx | 4 +-- .../sihirbaz/components/cta-sira-form.tsx | 11 +++++++- .../sihirbaz/components/hero-form.tsx | 1 + .../components/sihirbaz-cagri-karti.tsx | 18 ++++++++++++- src/lib/analitik.ts | 8 +++++- 11 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 src/app/paket/layout.tsx create mode 100644 src/features/odeme/components/paket-gorunum-olayi.tsx diff --git a/src/app/paket/layout.tsx b/src/app/paket/layout.tsx new file mode 100644 index 0000000..7620f3e --- /dev/null +++ b/src/app/paket/layout.tsx @@ -0,0 +1,16 @@ +import { PaketGorunumOlayi } from "@/features/odeme/components/paket-gorunum-olayi"; + +// Yalnızca ölçüm için: sayfanın kendisine dokunmadan paket_goruntulendi +// olayını bağlar (bkz. paket-gorunum-olayi.tsx). +export default function PaketLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + {children} + + + ); +} diff --git a/src/features/kullanici/components/giris-form.tsx b/src/features/kullanici/components/giris-form.tsx index 2a14fe6..1510ece 100644 --- a/src/features/kullanici/components/giris-form.tsx +++ b/src/features/kullanici/components/giris-form.tsx @@ -59,6 +59,7 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) { callbackURL, }); if (error) { + olay("giris_hata", { yontem: "google", status: error.status ?? 0 }); toast.error("Google ile giriş başarısız. Tekrar dener misin?"); setBusy(null); } @@ -81,6 +82,7 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) { }); setBusy(null); if (error) { + olay("giris_hata", { yontem: "eposta", status: error.status ?? 0 }); toast.error("Bağlantı gönderilemedi. Tekrar dener misin?"); return; } diff --git a/src/features/kullanici/components/user-nav.tsx b/src/features/kullanici/components/user-nav.tsx index 5b43f6a..9a45188 100644 --- a/src/features/kullanici/components/user-nav.tsx +++ b/src/features/kullanici/components/user-nav.tsx @@ -48,7 +48,7 @@ export async function UserNav() { diff --git a/src/features/liste/components/tercih-profili-kapisi.tsx b/src/features/liste/components/tercih-profili-kapisi.tsx index d9ca13a..7085430 100644 --- a/src/features/liste/components/tercih-profili-kapisi.tsx +++ b/src/features/liste/components/tercih-profili-kapisi.tsx @@ -163,6 +163,7 @@ function KapıIcerik({ sira_kovasi: siraKovasi(siraDeger), }); } catch { + olay("sira_form_hata", { kaynak: duzenle ? "duzenle" : "kapi" }); setHata("Bir şeyler ters gitti, tekrar dener misin?"); } finally { setYukleniyor(false); diff --git a/src/features/odeme/components/paket-gorunum-olayi.tsx b/src/features/odeme/components/paket-gorunum-olayi.tsx new file mode 100644 index 0000000..8d9a172 --- /dev/null +++ b/src/features/odeme/components/paket-gorunum-olayi.tsx @@ -0,0 +1,25 @@ +"use client"; + +// /paket görüntülenmesini ve ziyaretçiyi oraya getiren yüzeyi ölçer. +// +// Neden ayrı bileşen + layout: /paket sayfası sunucu bileşeni ve statik +// kalmalı. `useSearchParams` kullanılmaz — cacheComponents açıkken Suspense +// sınırı/CSR bailout gerektirir; parametre yalnızca ölçüm için lazım, mount +// sonrası window.location'dan okumak yeter. + +import { useEffect } from "react"; +import { olayBirKez } from "@/lib/analitik"; + +// Serbest metin analitiğe sızmasın: bilinmeyen değerler "diger"e düşer. +const KAYNAKLAR = ["kilit", "kredi_bitti", "nav"] as const; + +export function PaketGorunumOlayi() { + useEffect(() => { + const ham = new URLSearchParams(window.location.search).get("kaynak"); + const kaynak = KAYNAKLAR.find((k) => k === ham) ?? "diger"; + // Oturum başına kaynak başına 1 kez: yenileme sayıyı şişirmesin + olayBirKez("paket_goruntulendi", { kaynak }, kaynak); + }, []); + + return null; +} diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx index b5f36dc..c355e0b 100644 --- a/src/features/rapor/components/liste-uretici.tsx +++ b/src/features/rapor/components/liste-uretici.tsx @@ -84,9 +84,11 @@ export function ListeUretici({ } else if (sonuc.code === "AUTH") { router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`); } else { + olay("liste_uretim_hata", { kod: sonuc.code }); setDurum({ ad: "hata", mesaj: sonuc.error }); } } catch { + olay("liste_uretim_hata", { kod: "ISTISNA" }); setDurum({ ad: "hata", mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?", @@ -131,7 +133,7 @@ export function ListeUretici({ size="lg" className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600" > - + {hasPaket ? "+30 kredi yükle — 129 TL" : "Paketi aktive et — 299 TL"} diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index 026a86c..28feb08 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -285,7 +285,7 @@ export function ListemGovde({ asChild className="h-11 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600 sm:w-auto" > - + Analizleri aç — {paketFiyati} TL @@ -366,7 +366,7 @@ export function ListemGovde({ size="lg" className="mt-5 w-full cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600" > - + Kişisel analizi aç diff --git a/src/features/sihirbaz/components/cta-sira-form.tsx b/src/features/sihirbaz/components/cta-sira-form.tsx index d35a757..6003c8f 100644 --- a/src/features/sihirbaz/components/cta-sira-form.tsx +++ b/src/features/sihirbaz/components/cta-sira-form.tsx @@ -21,6 +21,7 @@ import { DrawerTitle, } from "@/components/ui/drawer"; import { SectionEyebrow } from "@/components/pixel-decor"; +import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { SihirbazAdimlarLazy, preloadSihirbazAdimlar, @@ -73,6 +74,10 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) { const [yukleniyor, setYukleniyor] = useState(false); const [hata, setHata] = useState(null); const [girisli, setGirisli] = useState(false); + // Huni girişi ölçümü: ref yalnızca profilsiz daldaki karta bağlanır; kayıtlı + // profilli ziyaretçi formu hiç görmediğinden sayılmaz. Sayfa ayrımı için + // özellik gerekmez — Rybbit olayın pathname'ini zaten yazar. + const formRef = useGorunumOlayi("cta_form_goruntulendi"); // Yazarken binlik ayracıyla biçimle (85000 → 85.000) function siralamaDegisti(ham: string) { @@ -108,6 +113,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) { sira_kovasi: siraKovasi(value), }); } catch { + olay("sira_form_hata", { kaynak: "cta" }); setHata("Bir şeyler ters gitti, tekrar dener misin?"); } finally { setYukleniyor(false); @@ -159,7 +165,10 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) { } return ( -
+

{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}

diff --git a/src/features/sihirbaz/components/hero-form.tsx b/src/features/sihirbaz/components/hero-form.tsx index 484746b..ec4f94e 100644 --- a/src/features/sihirbaz/components/hero-form.tsx +++ b/src/features/sihirbaz/components/hero-form.tsx @@ -85,6 +85,7 @@ export function HeroForm() { sira_kovasi: siraKovasi(value), }); } catch { + olay("sira_form_hata", { kaynak: "hero" }); setHata("Bir şeyler ters gitti, tekrar dener misin?"); } finally { setYukleniyor(false); diff --git a/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx b/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx index 2568f83..d5a3f90 100644 --- a/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx +++ b/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx @@ -9,7 +9,7 @@ import { useReducedMotion } from "motion/react"; import { Button } from "@/components/ui/button"; import { PixelField } from "@/components/pixel-decor"; import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; -import { olay } from "@/lib/analitik"; +import { olay, olayBirKez, siraKovasi } from "@/lib/analitik"; import type { SihirbazFacetleri } from "@/types/yokatlas"; import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler"; import { tercihProfiliYaz } from "@/features/sihirbaz/sihirbaz-profil"; @@ -82,6 +82,22 @@ export function SihirbazBolumu({ // görsün diye ref'te tutulur; olay dinleyicisi bir kez bağlanır. const bekleyenRef = useRef(null); + // Huni adımı: sonuç sayfası görüntülendi. URL sıralamayı taşıdığı için + // pathname maskeli (bkz. app/layout.tsx); huni bu olayla kurulur. Oturum + // başına girişli/anonim için birer kez — girişten dönen ziyaretçi ikinci + // kez "girisli" olarak sayılır. Ham sıra gönderilmez, yalnızca kovası. + useEffect(() => { + olayBirKez( + "sonuc_goruntulendi", + { + girisli: girisliMi ? "evet" : "hayir", + sira_kovasi: siraKovasi(sira), + }, + girisliMi ? "girisli" : "anonim", + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Mount sonrası storage'dan bekleyen seçimleri oku useEffect(() => { try { diff --git a/src/lib/analitik.ts b/src/lib/analitik.ts index c60c552..b7dedb5 100644 --- a/src/lib/analitik.ts +++ b/src/lib/analitik.ts @@ -30,7 +30,13 @@ export type OlayAdi = | "kilit_goruntulendi" | "kredi_bitti" | "tadimlik_goruntulendi" - | "kaydet_banner_goruntulendi"; + | "kaydet_banner_goruntulendi" + | "cta_form_goruntulendi" + | "sira_form_hata" + | "sonuc_goruntulendi" + | "paket_goruntulendi" + | "liste_uretim_hata" + | "giris_hata"; export type Ozellikler = Record; From e1bb4829fd182bb3580caccef388b652ab8c2448 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:49:31 +0300 Subject: [PATCH 3/4] =?UTF-8?q?fix(giris):=20429=20mesaj=C4=B1=20+=2060=20?= =?UTF-8?q?sn=20geri=20say=C4=B1m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hız sınırına takılan kullanıcı "Tekrar dener misin?" görüp sınırı uzatıyordu; 429'da "Çok sık denedin, bir dakika sonra tekrar dene" denir. "Yeniden gönder" geri sayımı 30 → 60 sn: sunucu tarafındaki adres başına 60 sn soğumayla hizalı (o değişiklik gelmese de zararsız). Co-Authored-By: Claude Fable 5.1 --- src/features/kullanici/components/giris-form.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/features/kullanici/components/giris-form.tsx b/src/features/kullanici/components/giris-form.tsx index 1510ece..6b51fd0 100644 --- a/src/features/kullanici/components/giris-form.tsx +++ b/src/features/kullanici/components/giris-form.tsx @@ -38,8 +38,10 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) { // Yeniden gönderim arası bekleme (sn) — istemeden spam'lenmesin const [bekleme, setBekleme] = useState(0); + // 60 sn: sunucunun adres başına gönderim soğumasıyla hizalı; daha kısa + // sayaçta "Yeniden gönder" sessizce yutulur, kullanıcı gelmeyen maili bekler. function geriSayimBaslat() { - setBekleme(30); + setBekleme(60); const sayac = setInterval(() => { setBekleme((s) => { if (s <= 1) { @@ -83,7 +85,12 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) { setBusy(null); if (error) { olay("giris_hata", { yontem: "eposta", status: error.status ?? 0 }); - toast.error("Bağlantı gönderilemedi. Tekrar dener misin?"); + // 429 = hız sınırı: "tekrar dene" demek sınırı daha da uzatır + toast.error( + error.status === 429 + ? "Çok sık denedin, bir dakika sonra tekrar dene" + : "Bağlantı gönderilemedi. Tekrar dener misin?", + ); return; } setSent(true); From c055e8272cd95185e46ffa2d6839589f30787748 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 04:50:04 +0300 Subject: [PATCH 4/4] =?UTF-8?q?chore(gizlilik):=20rybbit=20identify=20yaln?= =?UTF-8?q?=C4=B1z=20kullan=C4=B1c=C4=B1=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Bilal onayı gerekir] Girişli kullanıcının e-postası ve ismi Rybbit'e trait olarak gönderiliyordu. Artık yalnızca opak kullanıcı ID'si gider; anonim → girişli oturum birleştirmesi aynen çalışır. Rybbit'te zaten kayıtlı trait'ler bu commit'le silinmez (panelden ayrıca temizlenmeli). Bu commit dalın en sonunda ve bağımsızdır: istenmezse `git merge gece/p2-olcum~1` ile dışarıda bırakılır. Co-Authored-By: Claude Fable 5.1 --- src/components/rybbit-identify.tsx | 13 ++++--------- src/features/kullanici/components/user-nav.tsx | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/rybbit-identify.tsx b/src/components/rybbit-identify.tsx index 8d96748..7ebeb9e 100644 --- a/src/components/rybbit-identify.tsx +++ b/src/components/rybbit-identify.tsx @@ -12,24 +12,19 @@ import { kullaniciTanit, kullaniciTemizle } from "@/lib/analitik"; export function RybbitIdentify({ kullaniciId, - eposta, - isim, }: { /** null = oturum yok; o cihazdaki kimlik temizlenir (çıkış sonrası) */ kullaniciId: string | null; - eposta?: string | null; - isim?: string | null; }) { + // Yalnızca opak kullanıcı ID'si gider: e-posta/isim gibi kişisel veri + // üçüncü taraf analitiğe trait olarak GÖNDERİLMEZ. useEffect(() => { if (!kullaniciId) { kullaniciTemizle(); return; } - const traits: Record = {}; - if (eposta) traits.email = eposta; - if (isim) traits.name = isim; - kullaniciTanit(kullaniciId, traits); - }, [kullaniciId, eposta, isim]); + kullaniciTanit(kullaniciId); + }, [kullaniciId]); return null; } diff --git a/src/features/kullanici/components/user-nav.tsx b/src/features/kullanici/components/user-nav.tsx index 9a45188..2e16c79 100644 --- a/src/features/kullanici/components/user-nav.tsx +++ b/src/features/kullanici/components/user-nav.tsx @@ -44,7 +44,7 @@ export async function UserNav() { return (
- +