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>
185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
/**
|
||
* 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
|
||
}
|
||
}
|