Files
kolaytercih/src/lib/analitik.ts
bilalgursen a3cb058a3b
All checks were successful
Deploy / deploy (push) Successful in 7m45s
feat: update app.db and BACKLOG.md with new decisions and adjustments
- Updated the app.db file to reflect recent changes in the application state.
- Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates.
- Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
2026-10-06 17:17:49 +03:00

244 lines
8.4 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"
// Ü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<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
}
}