Files
kolaytercih/src/lib/analitik.ts
bilalgursen fa43392077 fix(analitik): görünüm hook'u callback ref + erken olay kuyruğu
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.<ad>[.<ek>]).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 04:47:24 +03:00

185 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, string | number>;
interface RybbitApi {
event: (ad: string, ozellikler?: Ozellikler) => void;
pageview: () => void;
identify: (kullaniciId: string, traits?: Record<string, string>) => void;
setTraits: (traits: Record<string, string>) => 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<typeof setInterval> | 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<string, string>,
): 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
}
}