Merge branch 'gece/p2-olcum' into gece/entegrasyon-test
This commit is contained in:
16
src/app/paket/layout.tsx
Normal file
16
src/app/paket/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { PaketGorunumOlayi } from "@/features/odeme/components/paket-gorunum-olayi";
|
||||||
|
|
||||||
|
// Yalnızca ölçüm için: sayfanın kendisine dokunmadan paket_goruntulendi
|
||||||
|
// olayını bağlar (bkz. paket-gorunum-olayi.tsx).
|
||||||
|
export default function PaketLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<PaketGorunumOlayi />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,24 +12,19 @@ import { kullaniciTanit, kullaniciTemizle } from "@/lib/analitik";
|
|||||||
|
|
||||||
export function RybbitIdentify({
|
export function RybbitIdentify({
|
||||||
kullaniciId,
|
kullaniciId,
|
||||||
eposta,
|
|
||||||
isim,
|
|
||||||
}: {
|
}: {
|
||||||
/** null = oturum yok; o cihazdaki kimlik temizlenir (çıkış sonrası) */
|
/** null = oturum yok; o cihazdaki kimlik temizlenir (çıkış sonrası) */
|
||||||
kullaniciId: string | null;
|
kullaniciId: string | null;
|
||||||
eposta?: string | null;
|
|
||||||
isim?: string | null;
|
|
||||||
}) {
|
}) {
|
||||||
|
// Yalnızca opak kullanıcı ID'si gider: e-posta/isim gibi kişisel veri
|
||||||
|
// üçüncü taraf analitiğe trait olarak GÖNDERİLMEZ.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kullaniciId) {
|
if (!kullaniciId) {
|
||||||
kullaniciTemizle();
|
kullaniciTemizle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const traits: Record<string, string> = {};
|
kullaniciTanit(kullaniciId);
|
||||||
if (eposta) traits.email = eposta;
|
}, [kullaniciId]);
|
||||||
if (isim) traits.name = isim;
|
|
||||||
kullaniciTanit(kullaniciId, traits);
|
|
||||||
}, [kullaniciId, eposta, isim]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,46 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, type RefObject } from "react";
|
import { useCallback, useEffect, useRef, type RefCallback } from "react";
|
||||||
import { olay, type OlayAdi, type Ozellikler } from "@/lib/analitik";
|
import {
|
||||||
|
olayAnahtari,
|
||||||
|
olayBirKez,
|
||||||
|
type OlayAdi,
|
||||||
|
type Ozellikler,
|
||||||
|
} from "@/lib/analitik";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bir elemanın viewport'a ilk girişinde analitik olayı atan ref hook'u.
|
* Bir elemanın viewport'a ilk girişinde analitik olayı atan ref hook'u.
|
||||||
* Gösterim olayları (hazir_panel_goruntulendi, kilit_goruntulendi vb.)
|
* Gösterim olayları (hazir_panel_goruntulendi, kilit_goruntulendi vb.)
|
||||||
* oturum başına en fazla 1 kez sayılmalı; tekilleştirme sessionStorage
|
* oturum başına en fazla 1 kez sayılmalı; tekilleştirme sessionStorage
|
||||||
* anahtarıyla yapılır. IntersectionObserver yoksa olay hiç atılmaz —
|
* anahtarıyla yapılır (bkz. olayBirKez). IntersectionObserver yoksa olay hiç
|
||||||
* "görüntülendi" ölçümünde görünürlüğü doğrulayamadığımız cihazı saymak
|
* atılmaz — "görüntülendi" ölçümünde görünürlüğü doğrulayamadığımız cihazı
|
||||||
* veriyi şişirir.
|
* saymak veriyi şişirir.
|
||||||
|
*
|
||||||
|
* Callback ref döner (RefObject değil): gözlem effect'te değil, DOM düğümü
|
||||||
|
* bağlandığı anda kurulur. Eleman ilk commit'te yoksa (kaydet-banner: mount
|
||||||
|
* sonrası açılır, üstelik portal içinde) ya da sökülüp yeni bir düğümle geri
|
||||||
|
* geliyorsa (tadımlık satırı: tip uymayınca gizlenip fetch sonrası yeniden
|
||||||
|
* takılır) useRef + effect deseni gözlemi hiç kurmuyordu.
|
||||||
*/
|
*/
|
||||||
export function useGorunumOlayi<T extends HTMLElement = HTMLElement>(
|
export function useGorunumOlayi<T extends HTMLElement = HTMLElement>(
|
||||||
ad: OlayAdi,
|
ad: OlayAdi,
|
||||||
ozellikler?: Ozellikler,
|
ozellikler?: Ozellikler,
|
||||||
/** Aynı olayın farklı yüzeyleri için tekilleştirme eki ("satir", "kart"). */
|
/** Aynı olayın farklı yüzeyleri için tekilleştirme eki ("satir", "kart"). */
|
||||||
anahtarEki?: string,
|
anahtarEki?: string,
|
||||||
): RefObject<T | null> {
|
): RefCallback<T> {
|
||||||
const ref = useRef<T>(null);
|
// Özellikler her render sonrası tazelenir; callback ref yalnızca ad/anahtar
|
||||||
// Özellikler her render sonrası tazelenir; observer effect'i yalnızca
|
// değişiminde yeniden kurulur (deps'e obje koymamak için ref).
|
||||||
// ad/anahtar değişiminde kurulur (deps'e obje koymamak için ref).
|
|
||||||
const ozelliklerRef = useRef(ozellikler);
|
const ozelliklerRef = useRef(ozellikler);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ozelliklerRef.current = ozellikler;
|
ozelliklerRef.current = ozellikler;
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
return useCallback(
|
||||||
const el = ref.current;
|
(el: T | null) => {
|
||||||
if (!el || typeof IntersectionObserver === "undefined") return;
|
if (!el || typeof IntersectionObserver === "undefined") return;
|
||||||
|
|
||||||
const anahtar = `kolaytercih.olay.${ad}${anahtarEki ? `.${anahtarEki}` : ""}`;
|
|
||||||
try {
|
try {
|
||||||
if (window.sessionStorage.getItem(anahtar)) return;
|
if (window.sessionStorage.getItem(olayAnahtari(ad, anahtarEki))) return;
|
||||||
} catch {
|
} catch {
|
||||||
// Storage engelliyse tekilleştirmeden vazgeç, olay yine atılabilir
|
// Storage engelliyse tekilleştirmeden vazgeç, olay yine atılabilir
|
||||||
}
|
}
|
||||||
@@ -39,16 +48,13 @@ export function useGorunumOlayi<T extends HTMLElement = HTMLElement>(
|
|||||||
const io = new IntersectionObserver((entries) => {
|
const io = new IntersectionObserver((entries) => {
|
||||||
if (!entries.some((e) => e.isIntersecting)) return;
|
if (!entries.some((e) => e.isIntersecting)) return;
|
||||||
io.disconnect();
|
io.disconnect();
|
||||||
try {
|
olayBirKez(ad, ozelliklerRef.current, anahtarEki);
|
||||||
window.sessionStorage.setItem(anahtar, "1");
|
|
||||||
} catch {
|
|
||||||
// yut
|
|
||||||
}
|
|
||||||
olay(ad, ozelliklerRef.current);
|
|
||||||
});
|
});
|
||||||
io.observe(el);
|
io.observe(el);
|
||||||
|
// React 19 ref temizliği: düğüm sökülünce (ya da ad/anahtar değişince)
|
||||||
|
// gözlemci bırakılır; yeni düğüm bağlanınca callback baştan çalışır.
|
||||||
return () => io.disconnect();
|
return () => io.disconnect();
|
||||||
}, [ad, anahtarEki]);
|
},
|
||||||
|
[ad, anahtarEki],
|
||||||
return ref;
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
|||||||
// Yeniden gönderim arası bekleme (sn) — istemeden spam'lenmesin
|
// Yeniden gönderim arası bekleme (sn) — istemeden spam'lenmesin
|
||||||
const [bekleme, setBekleme] = useState(0);
|
const [bekleme, setBekleme] = useState(0);
|
||||||
|
|
||||||
|
// 60 sn: sunucunun adres başına gönderim soğumasıyla hizalı; daha kısa
|
||||||
|
// sayaçta "Yeniden gönder" sessizce yutulur, kullanıcı gelmeyen maili bekler.
|
||||||
function geriSayimBaslat() {
|
function geriSayimBaslat() {
|
||||||
setBekleme(30);
|
setBekleme(60);
|
||||||
const sayac = setInterval(() => {
|
const sayac = setInterval(() => {
|
||||||
setBekleme((s) => {
|
setBekleme((s) => {
|
||||||
if (s <= 1) {
|
if (s <= 1) {
|
||||||
@@ -59,6 +61,7 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
|||||||
callbackURL,
|
callbackURL,
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
|
olay("giris_hata", { yontem: "google", status: error.status ?? 0 });
|
||||||
toast.error("Google ile giriş başarısız. Tekrar dener misin?");
|
toast.error("Google ile giriş başarısız. Tekrar dener misin?");
|
||||||
setBusy(null);
|
setBusy(null);
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,13 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
|||||||
});
|
});
|
||||||
setBusy(null);
|
setBusy(null);
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error("Bağlantı gönderilemedi. Tekrar dener misin?");
|
olay("giris_hata", { yontem: "eposta", status: error.status ?? 0 });
|
||||||
|
// 429 = hız sınırı: "tekrar dene" demek sınırı daha da uzatır
|
||||||
|
toast.error(
|
||||||
|
error.status === 429
|
||||||
|
? "Çok sık denedin, bir dakika sonra tekrar dene"
|
||||||
|
: "Bağlantı gönderilemedi. Tekrar dener misin?",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSent(true);
|
setSent(true);
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ export async function UserNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="order-2 flex shrink-0 items-center gap-1.5 sm:gap-2">
|
<div className="order-2 flex shrink-0 items-center gap-1.5 sm:gap-2">
|
||||||
<RybbitIdentify kullaniciId={u.id} eposta={u.email} isim={u.name} />
|
<RybbitIdentify kullaniciId={u.id} />
|
||||||
<ListeSenkron userId={u.id} />
|
<ListeSenkron userId={u.id} />
|
||||||
<ListemButonu />
|
<ListemButonu />
|
||||||
<Link
|
<Link
|
||||||
href="/paket"
|
href="/paket?kaynak=nav"
|
||||||
className="inline-flex h-9 items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50 sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
|
className="inline-flex h-9 items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50 sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
|
||||||
title="Kredilerin — yüklemek için tıkla"
|
title="Kredilerin — yüklemek için tıkla"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ function KapıIcerik({
|
|||||||
sira_kovasi: siraKovasi(siraDeger),
|
sira_kovasi: siraKovasi(siraDeger),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
olay("sira_form_hata", { kaynak: duzenle ? "duzenle" : "kapi" });
|
||||||
setHata("Bir şeyler ters gitti, tekrar dener misin?");
|
setHata("Bir şeyler ters gitti, tekrar dener misin?");
|
||||||
} finally {
|
} finally {
|
||||||
setYukleniyor(false);
|
setYukleniyor(false);
|
||||||
|
|||||||
25
src/features/odeme/components/paket-gorunum-olayi.tsx
Normal file
25
src/features/odeme/components/paket-gorunum-olayi.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -84,9 +84,11 @@ export function ListeUretici({
|
|||||||
} else if (sonuc.code === "AUTH") {
|
} else if (sonuc.code === "AUTH") {
|
||||||
router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`);
|
router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`);
|
||||||
} else {
|
} else {
|
||||||
|
olay("liste_uretim_hata", { kod: sonuc.code });
|
||||||
setDurum({ ad: "hata", mesaj: sonuc.error });
|
setDurum({ ad: "hata", mesaj: sonuc.error });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
olay("liste_uretim_hata", { kod: "ISTISNA" });
|
||||||
setDurum({
|
setDurum({
|
||||||
ad: "hata",
|
ad: "hata",
|
||||||
mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?",
|
mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?",
|
||||||
@@ -131,7 +133,7 @@ export function ListeUretici({
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
<Link href="/paket">
|
<Link href="/paket?kaynak=kredi_bitti">
|
||||||
{hasPaket ? "+30 kredi yükle — 129 TL" : "Paketi aktive et — 299 TL"}
|
{hasPaket ? "+30 kredi yükle — 129 TL" : "Paketi aktive et — 299 TL"}
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export function ListemGovde({
|
|||||||
asChild
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<Link href="/paket">
|
<Link href="/paket?kaynak=kilit">
|
||||||
Analizleri aç — {paketFiyati} TL
|
Analizleri aç — {paketFiyati} TL
|
||||||
<ArrowRight data-icon="inline-end" aria-hidden />
|
<ArrowRight data-icon="inline-end" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -366,7 +366,7 @@ export function ListemGovde({
|
|||||||
size="lg"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<Link href="/paket">
|
<Link href="/paket?kaynak=kilit">
|
||||||
Kişisel analizi aç
|
Kişisel analizi aç
|
||||||
<ArrowRight data-icon="inline-end" aria-hidden />
|
<ArrowRight data-icon="inline-end" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
import { SectionEyebrow } from "@/components/pixel-decor";
|
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||||
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import {
|
import {
|
||||||
SihirbazAdimlarLazy,
|
SihirbazAdimlarLazy,
|
||||||
preloadSihirbazAdimlar,
|
preloadSihirbazAdimlar,
|
||||||
@@ -73,6 +74,10 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
|||||||
const [yukleniyor, setYukleniyor] = useState(false);
|
const [yukleniyor, setYukleniyor] = useState(false);
|
||||||
const [hata, setHata] = useState<string | null>(null);
|
const [hata, setHata] = useState<string | null>(null);
|
||||||
const [girisli, setGirisli] = useState(false);
|
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<HTMLElement>("cta_form_goruntulendi");
|
||||||
|
|
||||||
// Yazarken binlik ayracıyla biçimle (85000 → 85.000)
|
// Yazarken binlik ayracıyla biçimle (85000 → 85.000)
|
||||||
function siralamaDegisti(ham: string) {
|
function siralamaDegisti(ham: string) {
|
||||||
@@ -108,6 +113,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
|||||||
sira_kovasi: siraKovasi(value),
|
sira_kovasi: siraKovasi(value),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
olay("sira_form_hata", { kaynak: "cta" });
|
||||||
setHata("Bir şeyler ters gitti, tekrar dener misin?");
|
setHata("Bir şeyler ters gitti, tekrar dener misin?");
|
||||||
} finally {
|
} finally {
|
||||||
setYukleniyor(false);
|
setYukleniyor(false);
|
||||||
@@ -159,7 +165,10 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8">
|
<section
|
||||||
|
ref={formRef}
|
||||||
|
className="rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8"
|
||||||
|
>
|
||||||
<h2 className="font-heading text-xl font-bold text-slate-900">
|
<h2 className="font-heading text-xl font-bold text-slate-900">
|
||||||
{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}
|
{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export function HeroForm() {
|
|||||||
sira_kovasi: siraKovasi(value),
|
sira_kovasi: siraKovasi(value),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
olay("sira_form_hata", { kaynak: "hero" });
|
||||||
setHata("Bir şeyler ters gitti, tekrar dener misin?");
|
setHata("Bir şeyler ters gitti, tekrar dener misin?");
|
||||||
} finally {
|
} finally {
|
||||||
setYukleniyor(false);
|
setYukleniyor(false);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useReducedMotion } from "motion/react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PixelField } from "@/components/pixel-decor";
|
import { PixelField } from "@/components/pixel-decor";
|
||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
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 type { SihirbazFacetleri } from "@/types/yokatlas";
|
||||||
import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { tercihProfiliYaz } from "@/features/sihirbaz/sihirbaz-profil";
|
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.
|
// görsün diye ref'te tutulur; olay dinleyicisi bir kez bağlanır.
|
||||||
const bekleyenRef = useRef<BekleyenSecim | null>(null);
|
const bekleyenRef = useRef<BekleyenSecim | null>(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
|
// Mount sonrası storage'dan bekleyen seçimleri oku
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
*
|
*
|
||||||
* Script yalnızca prod build'de yükleniyor (bkz. app/layout.tsx) ve reklam
|
* 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ı
|
* engelleyiciler tarafından bloklanabiliyor; bu yüzden buradaki her çağrı
|
||||||
* `window.rybbit` yoksa sessizce hiçbir şey yapmaz — çağıran taraf asla
|
* `window.rybbit` yoksa kullanıcıya hiçbir şey hissettirmez (olaylar kısa
|
||||||
* varlık kontrolü yapmak zorunda kalmasın.
|
* 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
|
* KURAL: Öğrencinin ham başarı sıralaması analitiğe GÖNDERİLMEZ. Sıralama
|
||||||
* her zaman `siraKovasi()` ile bantlanır.
|
* her zaman `siraKovasi()` ile bantlanır.
|
||||||
@@ -29,7 +30,13 @@ export type OlayAdi =
|
|||||||
| "kilit_goruntulendi"
|
| "kilit_goruntulendi"
|
||||||
| "kredi_bitti"
|
| "kredi_bitti"
|
||||||
| "tadimlik_goruntulendi"
|
| "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<string, string | number>;
|
export type Ozellikler = Record<string, string | number>;
|
||||||
|
|
||||||
@@ -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<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 {
|
export function olay(ad: OlayAdi, ozellikler?: Ozellikler): void {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
try {
|
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 {
|
} catch {
|
||||||
// Analitik hiçbir koşulda kullanıcı akışını bozmaz
|
// 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ı
|
* Ham sıralamayı analitik için bantlar. Panelde "hangi sıralama bandı
|
||||||
* dönüşüyor" sorusunu yanıtlar ama tekil öğrenciyi işaret etmez.
|
* dönüşüyor" sorusunu yanıtlar ama tekil öğrenciyi işaret etmez.
|
||||||
|
|||||||
Reference in New Issue
Block a user