/** * Rybbit analitiği için ince sarmalayıcı. * * 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 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. */ /** Ölçtüğümüz huni olayları. Yeni olay eklerken bu birliği genişlet. */ export type OlayAdi = | "sira_girildi" | "sihirbaz_adim" | "sihirbaz_tamamlandi" | "giris_denendi" // Üretim denemesinin paydası: /listem?uret=1 ile üretim gövdesi mount // olduğunda atılır (sihirbaz, hazır panel CTA'sı, giriş dönüşü, boş durum // CTA'sı ve ödeme sonrası kartı — hangisi olduğu `kaynak` özelliğinde). // Bilerek `olayBirKez` KULLANILMAZ: aynı oturumdaki ikinci deneme de // sayılmalı, yoksa payda başarıdan (liste_olusturuldu) küçük kalır. | "liste_uretim_basladi" | "liste_olusturuldu" | "liste_guncellendi" | "liste_revize" // Seri kırılması: bu düzeltmenin canlıya çıktığı tarihten önce = + // tıklaması (kapı açılsa da atılıyordu), sonrası = gerçek ekleme // ({yol: "dogrudan" | "kapi"}); seriler karşılaştırılamaz. | "program_eklendi" // Program ayrıntı çekmecesi (eski adı netler_goruntulendi; 6 Eki 2026 // çekmece netlerden tüm ayrıntıya genişledi, seri yeni adla başlar) | "program_ayrinti_goruntulendi" | "danisman_sorusu" | "katalog_arama" | "odeme_baslatildi" | "hazir_panel_goruntulendi" | "giris_cta_tiklandi" | "kilit_goruntulendi" | "kredi_bitti" | "tadimlik_goruntulendi" | "kaydet_banner_goruntulendi" | "cta_form_goruntulendi" | "sira_form_hata" | "sonuc_goruntulendi" | "paket_goruntulendi" | "liste_uretim_hata" | "giris_hata" // Profil kapısı (sıra + sihirbaz modalı). kapi_acildi {tetik}: her açılış. // kapi_kapatildi {adim, tetik}: yalnız TAMAMLANMADAN kapanış (X, dışarı // tıklama, Vazgeç); tamamlanan kapıda atılmaz. | "kapi_acildi" | "kapi_kapatildi" // Danışman profili: yalnız ALAN ADI gider, değer gitmez (KVKK: analitiğe // profil değeri sızmaz). kaydedildi = model aracı çağırıp alanı yazdı // (istemci cevap sonrası profili çekip farkı görür); silindi = çipteki çarpı. | "profil_alani_kaydedildi" | "profil_alani_silindi"; export type Ozellikler = Record; interface RybbitApi { event: (ad: string, ozellikler?: Ozellikler) => void; pageview: () => void; identify: (kullaniciId: string, traits?: Record) => void; setTraits: (traits: Record) => void; clearUserId: () => void; getUserId: () => string | null; } declare global { interface Window { rybbit?: RybbitApi; } } // 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]> = []; // Script'ten önce gelen identify için tek "son değer" slotu: sert yüklemede // (OAuth / e-posta linki dönüşü) RybbitIdentify effect'i de script'ten önce // koşar. Kuyruk değil slot — yalnız en son kimlik anlamlıdır. let bekleyenKimlik: [string, Record | undefined] | null = null; let zamanlayici: ReturnType | null = null; let denemeSayisi = 0; let kuyrukKapali = false; function zamanlayiciyiDurdur(): void { if (zamanlayici !== null) clearInterval(zamanlayici); zamanlayici = null; } /** Önce bekleyen kimliği uygular, sonra olayları geliş sırasıyla gönderir * (kuyruktaki olaylar anonim profile değil tanımlı kullanıcıya yazılsın). */ function kuyruguBosalt(rybbit: RybbitApi): void { const bekleyenler = kuyruk; const kimlik = bekleyenKimlik; kuyruk = []; bekleyenKimlik = null; zamanlayiciyiDurdur(); if (kimlik) { try { rybbit.identify(kimlik[0], kimlik[1]); } catch { // yut } } for (const [ad, ozellikler] of bekleyenler) { try { rybbit.event(ad, ozellikler); } catch { // yut } } } /** Script'i bekleyen yoklamayı başlatır (olay kuyruğu ve kimlik slotu ortak). */ function zamanlayiciyiBaslat(): void { if (zamanlayici !== null) return; zamanlayici = setInterval(() => { denemeSayisi += 1; if (window.rybbit) { kuyruguBosalt(window.rybbit); } else if (denemeSayisi >= DENEME_SINIRI) { // Script gelmedi: bekleyenleri at, sonraki çağrılar eski no-op'a dönsün kuyruk = []; bekleyenKimlik = null; kuyrukKapali = true; zamanlayiciyiDurdur(); } }, DENEME_ARALIGI_MS); } function kuyrugaEkle(ad: OlayAdi, ozellikler?: Ozellikler): void { if (kuyrukKapali || kuyruk.length >= KUYRUK_SINIRI) return; kuyruk.push([ad, ozellikler]); zamanlayiciyiBaslat(); } /** Ö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 { if (!window.rybbit) { kuyrugaEkle(ad, ozellikler); return; } // Sıra bozulmasın: bekleyen (kimlik ya da olay) varsa önce onlar gider if (kuyruk.length > 0 || bekleyenKimlik) 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. */ export function siraKovasi(sira: number): string { if (sira <= 10_000) return "0-10 bin"; if (sira <= 50_000) return "10-50 bin"; if (sira <= 100_000) return "50-100 bin"; if (sira <= 250_000) return "100-250 bin"; if (sira <= 500_000) return "250-500 bin"; return "500 bin+"; } /** * Giriş yapan kullanıcıyı eşler. İlk çağrıda o cihazdaki önceki anonim * olaylar geriye dönük olarak bu kullanıcıya bağlanır (sihirbazı doldurup * sonra giriş yapan öğrencinin yolculuğu tek profilde birleşsin diye). * Script henüz yoksa kimlik tek slotta bekler; kuyruk boşaltılırken * olaylardan ÖNCE uygulanır. */ export function kullaniciTanit( kullaniciId: string, traits?: Record, ): void { if (typeof window === "undefined") return; try { if (!window.rybbit) { if (kuyrukKapali) return; bekleyenKimlik = [kullaniciId, traits]; zamanlayiciyiBaslat(); return; } // Doğrudan uygulanır; kuyrukta olay bekliyorsa sonraki boşaltmada bu // kimlikle gider. Eski slot değeri geçersiz kalmasın diye silinir. bekleyenKimlik = null; window.rybbit.identify(kullaniciId, traits); } catch { // yut } } /** Çıkışta kimliği temizler (bekleyen kimlik slotu dahil). */ export function kullaniciTemizle(): void { if (typeof window === "undefined") return; bekleyenKimlik = null; try { window.rybbit?.clearUserId(); } catch { // yut } }