/** * 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" | "liste_olusturuldu" | "liste_guncellendi" | "liste_revize" | "program_eklendi" | "netler_goruntulendi" | "danisman_sorusu" | "katalog_arama" | "odeme_baslatildi" | "hazir_panel_goruntulendi" | "giris_cta_tiklandi" | "kilit_goruntulendi" | "kredi_bitti" | "tadimlik_goruntulendi" | "kaydet_banner_goruntulendi"; 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]> = []; 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 { 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. */ 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). */ export function kullaniciTanit( kullaniciId: string, traits?: Record, ): void { if (typeof window === "undefined") return; try { window.rybbit?.identify(kullaniciId, traits); } catch { // yut } } /** Çıkışta kimliği temizler. */ export function kullaniciTemizle(): void { if (typeof window === "undefined") return; try { window.rybbit?.clearUserId(); } catch { // yut } }