Bugüne kadar yalnız başarı (liste_olusturuldu) ölçülüyordu; kaç kişinin üretimi denediği bilinmiyordu. /listem?uret=1 ile üretim gövdesi mount olduğunda yeni bileşen `liste_uretim_basladi` atar. Olay bilerek liste-uretici.tsx'e değil, listem-icerik.tsx'te ona kardeş mount edilen yeni bir istemci bileşenine kondu (o dosya bu turda başka bir işin altında). İkisi aynı `uret` dalında render edildiği için koşulları birebir aynı. `kaynak` URL'den, yani kullanıcı denetiminden geldiği için allowlist'ten geçirilir; listede olmayan değer "bilinmiyor" raporlanır. Giriş round-trip'inde değerin kaybolmaması için geriYol da kaynağı taşır — `uret=1` her zaman ilk parametre kalır (giris-karti.tsx:31 sözleşmesi). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
237 lines
8.0 KiB
TypeScript
237 lines
8.0 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"
|
||
// Ü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"
|
||
| "netler_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";
|
||
|
||
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]> = [];
|
||
// 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<string, string> | undefined] | null = null;
|
||
let zamanlayici: ReturnType<typeof setInterval> | 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<string, string>,
|
||
): 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
|
||
}
|
||
}
|