Integrate Rybbit analytics and enhance user tracking features
All checks were successful
Deploy / deploy (push) Successful in 16m7s

- Added Rybbit analytics configuration in .mcp.json for improved data collection.
- Updated layout.tsx to conditionally load the Rybbit analytics script in production.
- Enhanced user interaction tracking in various components, including GirisForm, RevizyonKutusu, and SatinAlForm, by logging specific events.
- Updated privacy policy page to reflect new data handling practices and added a section on service providers and data transfer.
- Improved user navigation and experience by integrating Rybbit's live visitor widget in the site footer.
This commit is contained in:
bilalgursen
2026-08-02 19:29:42 +03:00
parent cc9686147e
commit 0b92f999bc
22 changed files with 499 additions and 16 deletions

View File

@@ -26,6 +26,7 @@ import {
preloadSihirbazAdimlar,
} from "@/components/sihirbaz-adimlar-lazy";
import { authClient } from "@/lib/auth-client";
import { olay, siraKovasi } from "@/lib/analitik";
import { useTercihProfili } from "@/components/manuel-liste/profil-kapisi-store";
import type { SihirbazFacetleri } from "@/lib/db";
import {
@@ -103,6 +104,11 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
setSira(value);
setFacetler(veri.facetler);
setAdim("sihirbaz");
olay("sira_girildi", {
kaynak: "cta",
tur,
sira_kovasi: siraKovasi(value),
});
} catch {
setHata("Bir şeyler ters gitti, tekrar dener misin?");
} finally {

View File

@@ -17,6 +17,7 @@ import {
preloadSihirbazAdimlar,
} from "@/components/sihirbaz-adimlar-lazy";
import { authClient } from "@/lib/auth-client";
import { olay, siraKovasi } from "@/lib/analitik";
import {
profilDuzenlemeyiAc,
useTercihProfili,
@@ -80,6 +81,11 @@ export function HeroForm() {
setSira(value);
setFacetler(veri.facetler);
setModalAcik(true);
olay("sira_girildi", {
kaynak: "hero",
tur,
sira_kovasi: siraKovasi(value),
});
} catch {
setHata("Bir şeyler ters gitti, tekrar dener misin?");
} finally {

View File

@@ -14,6 +14,7 @@ import {
useTercihProfili,
} from "@/components/manuel-liste/profil-kapisi-store";
import { PUAN_TURU_ETIKET } from "@/lib/sihirbaz";
import { olay } from "@/lib/analitik";
import {
ArrowRight,
BookOpenText,
@@ -96,6 +97,11 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
const inputRef = useRef<HTMLInputElement>(null);
const kaydirmaRef = useRef<HTMLDivElement>(null);
const [kenarBlur, setKenarBlur] = useState({ ust: false, alt: false });
// Kapanışta gönderilecek son arama özeti (bkz. aşağıdaki fetch efekti)
const sonAramaRef = useRef<{
sorgu_uzunlugu: number;
sonuc_sayisi: number;
} | null>(null);
const temizSorgu = sorgu.trim();
const aramaAktif = temizSorgu.length >= 2;
@@ -163,7 +169,14 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
{ signal: controller.signal },
);
if (!response.ok) throw new Error("Arama başarısız");
setSonuclar((await response.json()) as AramaSonuclari);
const veri = (await response.json()) as AramaSonuclari;
setSonuclar(veri);
// Olay burada değil kapanışta gönderilir; fetch her tuş vuruşunda
// (160ms debounce) tetikleniyor, aramayı oturum başına bir kez sayarız.
sonAramaRef.current = {
sorgu_uzunlugu: temizSorgu.length,
sonuc_sayisi: veri.bolumler.length + veri.universiteler.length,
};
} catch (hata) {
if (!(hata instanceof DOMException && hata.name === "AbortError")) {
setSonuclar(BOS_SONUCLAR);
@@ -207,6 +220,15 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
}
}
// Arama oturumu kapandığında tek bir olay — hiç arama yapılmadıysa sessiz
useEffect(() => {
if (acik) return;
const son = sonAramaRef.current;
if (!son) return;
sonAramaRef.current = null;
olay("katalog_arama", son);
}, [acik]);
function kapat() {
setAcik(false);
}

View File

@@ -3,6 +3,7 @@
import { Check, Plus } from "lucide-react";
import { toast } from "sonner";
import type { Program } from "@/lib/db";
import { olay } from "@/lib/analitik";
import {
listedeMi,
MANUEL_LISTE_MAX,
@@ -48,6 +49,8 @@ export function ProgramEkleButonu({
toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`);
} else if (sonuc.zatenVar) {
toast.info("Bu program zaten seçtiklerinde.");
} else {
olay("program_eklendi", { kaynak });
}
}

View File

@@ -27,6 +27,7 @@ import {
preloadSihirbazAdimlar,
} from "@/components/sihirbaz-adimlar-lazy";
import type { SihirbazFacetleri } from "@/lib/db";
import { olay, siraKovasi } from "@/lib/analitik";
import {
tercihProfiliYaz,
type SihirbazSecimleri,
@@ -142,6 +143,11 @@ function KapıIcerik({
setTur(turDeger);
setFacetler(veri.facetler);
adimaGec("sihirbaz");
olay("sira_girildi", {
kaynak: duzenle ? "duzenle" : "kapi",
tur: turDeger,
sira_kovasi: siraKovasi(siraDeger),
});
} catch {
setHata("Bir şeyler ters gitti, tekrar dener misin?");
} finally {

View File

@@ -0,0 +1,35 @@
"use client";
// Rybbit'e giriş yapan kullanıcıyı bildirir.
//
// Neden burada: oturum zaten UserNav'da (sunucu bileşeni, Suspense içinde)
// çözülüyor. Layout'a authClient.useSession() kullanan global bir bileşen
// koymak landing'i statik olmaktan çıkarır ve ziyaret başına gereksiz bir
// auth turu attırırdı — bkz. hero-form.tsx'teki aynı gerekçe.
import { useEffect } from "react";
import { kullaniciTanit, kullaniciTemizle } from "@/lib/analitik";
export function RybbitIdentify({
kullaniciId,
eposta,
isim,
}: {
/** null = oturum yok; o cihazdaki kimlik temizlenir (çıkış sonrası) */
kullaniciId: string | null;
eposta?: string | null;
isim?: string | null;
}) {
useEffect(() => {
if (!kullaniciId) {
kullaniciTemizle();
return;
}
const traits: Record<string, string> = {};
if (eposta) traits.email = eposta;
if (isim) traits.name = isim;
kullaniciTanit(kullaniciId, traits);
}, [kullaniciId, eposta, isim]);
return null;
}

View File

@@ -17,6 +17,7 @@ import { TransitionPanel } from "@/components/core/transition-panel";
import { Button } from "@/components/ui/button";
import { IlSecimHaritasi } from "@/components/il-secim-haritasi";
import { normalizeIlAdi } from "@/lib/harita";
import { olay } from "@/lib/analitik";
import { trBaslikDuzeni } from "@/lib/slug";
import type { SihirbazFacetleri } from "@/lib/db";
import {
@@ -147,6 +148,8 @@ export function SihirbazAdimlar({
setYon(yeniAdim > adim ? 1 : -1);
setGecisBasladi(true);
setAdim(yeniAdim);
// Yalnızca ileri geçişler huni adımı sayılır; geri dönüşler tekrar saymasın
if (yeniAdim > adim) olay("sihirbaz_adim", { adim: yeniAdim + 1 });
}
const panelYuksekligi =
@@ -377,9 +380,15 @@ export function SihirbazAdimlar({
) : (
<Button
type="button"
onClick={() =>
onTamamla({ kategoriler, iller, universiteTipi, oncelikler })
}
onClick={() => {
olay("sihirbaz_tamamlandi", {
kategori_sayisi: kategoriler.length,
il_sayisi: iller.length,
uni_tipi: universiteTipi,
oncelik_sayisi: oncelikler.length,
});
onTamamla({ kategoriler, iller, universiteTipi, oncelikler });
}}
disabled={kategoriler.length === 0}
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>

View File

@@ -155,6 +155,17 @@ export function SiteFooter() {
İletişim
</Link>
</nav>
{/* Rybbit canlı ziyaretçi rozeti — sola yaslı */}
<div className="mt-6 flex justify-start">
<iframe
src="https://rybbit.kolaytercih.com/widget/f2cb042c657e?variant=inline&theme=dark"
style={{ border: 0, width: 220, height: 36 }}
loading="lazy"
title="Canlı ziyaretçi"
scrolling="no"
/>
</div>
</div>
</div>
</footer>

View File

@@ -3,6 +3,7 @@ import { Coins } from "lucide-react";
import { getCurrentUser } from "@/lib/session";
import { Button } from "@/components/ui/button";
import { ListemButonu } from "@/components/manuel-liste/listem-butonu";
import { RybbitIdentify } from "@/components/rybbit-identify";
import { SignOutButton } from "./sign-out-button";
/**
@@ -27,6 +28,7 @@ export async function UserNav() {
if (!u) {
return (
<div className="order-2 flex shrink-0 items-center gap-1.5 sm:gap-2">
<RybbitIdentify kullaniciId={null} />
<ListemButonu />
<Button
asChild
@@ -40,6 +42,7 @@ export async function UserNav() {
return (
<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} />
<ListemButonu />
<Link
href="/paket"