-
+
diff --git a/src/features/liste/components/tercih-profili-kapisi.tsx b/src/features/liste/components/tercih-profili-kapisi.tsx
index d9ca13a..7085430 100644
--- a/src/features/liste/components/tercih-profili-kapisi.tsx
+++ b/src/features/liste/components/tercih-profili-kapisi.tsx
@@ -163,6 +163,7 @@ function KapıIcerik({
sira_kovasi: siraKovasi(siraDeger),
});
} catch {
+ olay("sira_form_hata", { kaynak: duzenle ? "duzenle" : "kapi" });
setHata("Bir şeyler ters gitti, tekrar dener misin?");
} finally {
setYukleniyor(false);
diff --git a/src/features/odeme/components/paket-gorunum-olayi.tsx b/src/features/odeme/components/paket-gorunum-olayi.tsx
new file mode 100644
index 0000000..8d9a172
--- /dev/null
+++ b/src/features/odeme/components/paket-gorunum-olayi.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+// /paket görüntülenmesini ve ziyaretçiyi oraya getiren yüzeyi ölçer.
+//
+// Neden ayrı bileşen + layout: /paket sayfası sunucu bileşeni ve statik
+// kalmalı. `useSearchParams` kullanılmaz — cacheComponents açıkken Suspense
+// sınırı/CSR bailout gerektirir; parametre yalnızca ölçüm için lazım, mount
+// sonrası window.location'dan okumak yeter.
+
+import { useEffect } from "react";
+import { olayBirKez } from "@/lib/analitik";
+
+// Serbest metin analitiğe sızmasın: bilinmeyen değerler "diger"e düşer.
+const KAYNAKLAR = ["kilit", "kredi_bitti", "nav"] as const;
+
+export function PaketGorunumOlayi() {
+ useEffect(() => {
+ const ham = new URLSearchParams(window.location.search).get("kaynak");
+ const kaynak = KAYNAKLAR.find((k) => k === ham) ?? "diger";
+ // Oturum başına kaynak başına 1 kez: yenileme sayıyı şişirmesin
+ olayBirKez("paket_goruntulendi", { kaynak }, kaynak);
+ }, []);
+
+ return null;
+}
diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx
index b5f36dc..c355e0b 100644
--- a/src/features/rapor/components/liste-uretici.tsx
+++ b/src/features/rapor/components/liste-uretici.tsx
@@ -84,9 +84,11 @@ export function ListeUretici({
} else if (sonuc.code === "AUTH") {
router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`);
} else {
+ olay("liste_uretim_hata", { kod: sonuc.code });
setDurum({ ad: "hata", mesaj: sonuc.error });
}
} catch {
+ olay("liste_uretim_hata", { kod: "ISTISNA" });
setDurum({
ad: "hata",
mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?",
@@ -131,7 +133,7 @@ export function ListeUretici({
size="lg"
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
-
+
{hasPaket ? "+30 kredi yükle — 129 TL" : "Paketi aktive et — 299 TL"}
diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx
index 026a86c..28feb08 100644
--- a/src/features/rapor/components/listem-govde.tsx
+++ b/src/features/rapor/components/listem-govde.tsx
@@ -285,7 +285,7 @@ export function ListemGovde({
asChild
className="h-11 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600 sm:w-auto"
>
-
+
Analizleri aç — {paketFiyati} TL
@@ -366,7 +366,7 @@ export function ListemGovde({
size="lg"
className="mt-5 w-full cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600"
>
-
+
Kişisel analizi aç
diff --git a/src/features/sihirbaz/components/cta-sira-form.tsx b/src/features/sihirbaz/components/cta-sira-form.tsx
index d35a757..6003c8f 100644
--- a/src/features/sihirbaz/components/cta-sira-form.tsx
+++ b/src/features/sihirbaz/components/cta-sira-form.tsx
@@ -21,6 +21,7 @@ import {
DrawerTitle,
} from "@/components/ui/drawer";
import { SectionEyebrow } from "@/components/pixel-decor";
+import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import {
SihirbazAdimlarLazy,
preloadSihirbazAdimlar,
@@ -73,6 +74,10 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
const [yukleniyor, setYukleniyor] = useState(false);
const [hata, setHata] = useState
(null);
const [girisli, setGirisli] = useState(false);
+ // Huni girişi ölçümü: ref yalnızca profilsiz daldaki karta bağlanır; kayıtlı
+ // profilli ziyaretçi formu hiç görmediğinden sayılmaz. Sayfa ayrımı için
+ // özellik gerekmez — Rybbit olayın pathname'ini zaten yazar.
+ const formRef = useGorunumOlayi("cta_form_goruntulendi");
// Yazarken binlik ayracıyla biçimle (85000 → 85.000)
function siralamaDegisti(ham: string) {
@@ -108,6 +113,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
sira_kovasi: siraKovasi(value),
});
} catch {
+ olay("sira_form_hata", { kaynak: "cta" });
setHata("Bir şeyler ters gitti, tekrar dener misin?");
} finally {
setYukleniyor(false);
@@ -159,7 +165,10 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
}
return (
-
+
{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}
diff --git a/src/features/sihirbaz/components/hero-form.tsx b/src/features/sihirbaz/components/hero-form.tsx
index 484746b..ec4f94e 100644
--- a/src/features/sihirbaz/components/hero-form.tsx
+++ b/src/features/sihirbaz/components/hero-form.tsx
@@ -85,6 +85,7 @@ export function HeroForm() {
sira_kovasi: siraKovasi(value),
});
} catch {
+ olay("sira_form_hata", { kaynak: "hero" });
setHata("Bir şeyler ters gitti, tekrar dener misin?");
} finally {
setYukleniyor(false);
diff --git a/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx b/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx
index 2568f83..d5a3f90 100644
--- a/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx
+++ b/src/features/sihirbaz/components/sihirbaz-cagri-karti.tsx
@@ -9,7 +9,7 @@ import { useReducedMotion } from "motion/react";
import { Button } from "@/components/ui/button";
import { PixelField } from "@/components/pixel-decor";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
-import { olay } from "@/lib/analitik";
+import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
import type { SihirbazFacetleri } from "@/types/yokatlas";
import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliYaz } from "@/features/sihirbaz/sihirbaz-profil";
@@ -82,6 +82,22 @@ export function SihirbazBolumu({
// görsün diye ref'te tutulur; olay dinleyicisi bir kez bağlanır.
const bekleyenRef = useRef(null);
+ // Huni adımı: sonuç sayfası görüntülendi. URL sıralamayı taşıdığı için
+ // pathname maskeli (bkz. app/layout.tsx); huni bu olayla kurulur. Oturum
+ // başına girişli/anonim için birer kez — girişten dönen ziyaretçi ikinci
+ // kez "girisli" olarak sayılır. Ham sıra gönderilmez, yalnızca kovası.
+ useEffect(() => {
+ olayBirKez(
+ "sonuc_goruntulendi",
+ {
+ girisli: girisliMi ? "evet" : "hayir",
+ sira_kovasi: siraKovasi(sira),
+ },
+ girisliMi ? "girisli" : "anonim",
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
// Mount sonrası storage'dan bekleyen seçimleri oku
useEffect(() => {
try {
diff --git a/src/lib/analitik.ts b/src/lib/analitik.ts
index 2ed81cb..b7dedb5 100644
--- a/src/lib/analitik.ts
+++ b/src/lib/analitik.ts
@@ -3,8 +3,9 @@
*
* 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 sessizce hiçbir şey yapmaz — çağıran taraf asla
- * varlık kontrolü yapmak zorunda kalmasın.
+ * `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.
@@ -29,7 +30,13 @@ export type OlayAdi =
| "kilit_goruntulendi"
| "kredi_bitti"
| "tadimlik_goruntulendi"
- | "kaydet_banner_goruntulendi";
+ | "kaydet_banner_goruntulendi"
+ | "cta_form_goruntulendi"
+ | "sira_form_hata"
+ | "sonuc_goruntulendi"
+ | "paket_goruntulendi"
+ | "liste_uretim_hata"
+ | "giris_hata";
export type Ozellikler = Record;
@@ -48,16 +55,100 @@ declare global {
}
}
-/** Özel olay gönderir. Script yoksa no-op. */
+// 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 {
- window.rybbit?.event(ad, ozellikler);
+ 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.