feat(analitik): huni olayları

Huninin kör noktalarına altı olay (adlar kalıcı sözleşme):
- cta_form_goruntulendi: programatik sayfalardaki CTA kartı, yalnız
  profilsiz dalda (görünüm hook'u; sayfa ayrımı Rybbit pathname'inden)
- sira_form_hata {kaynak: hero|cta|kapi|duzenle}: facet isteği hatası;
  geçersiz sayı doğrulaması sayılmaz
- sonuc_goruntulendi {girisli, sira_kovasi}: /sonuc pathname'i maskeli,
  huni adımı bu olayla kurulur; oturum başına girişli/anonim birer kez
- paket_goruntulendi {kaynak: kilit|kredi_bitti|nav|diger}: sayfaya
  dokunmadan yeni paket/layout.tsx + client bileşenle; /paket linklerine
  ?kaynak= eklendi (listem kilit kartları, kredi-yetersiz, nav kredi pill'i)
- liste_uretim_hata {kod: HATA|ISTISNA}: AUTH/KREDI/PAKET dalları atmaz
- giris_hata {yontem: eposta|google, status}

Ham sıra ve e-posta hiçbir olaya girmez; sıra yalnız siraKovasi() ile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 04:49:09 +03:00
parent fa43392077
commit cf19903e19
11 changed files with 85 additions and 7 deletions

16
src/app/paket/layout.tsx Normal file
View 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 />
</>
);
}

View File

@@ -59,6 +59,7 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
callbackURL,
});
if (error) {
olay("giris_hata", { yontem: "google", status: error.status ?? 0 });
toast.error("Google ile giriş başarısız. Tekrar dener misin?");
setBusy(null);
}
@@ -81,6 +82,7 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
});
setBusy(null);
if (error) {
olay("giris_hata", { yontem: "eposta", status: error.status ?? 0 });
toast.error("Bağlantı gönderilemedi. Tekrar dener misin?");
return;
}

View File

@@ -48,7 +48,7 @@ export async function UserNav() {
<ListeSenkron userId={u.id} />
<ListemButonu />
<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"
title="Kredilerin — yüklemek için tıkla"
>

View File

@@ -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);

View 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;
}

View File

@@ -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"
>
<Link href="/paket">
<Link href="/paket?kaynak=kredi_bitti">
{hasPaket ? "+30 kredi yükle — 129 TL" : "Paketi aktive et — 299 TL"}
<ArrowRight className="size-4" aria-hidden />
</Link>

View File

@@ -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"
>
<Link href="/paket">
<Link href="/paket?kaynak=kilit">
Analizleri aç — {paketFiyati} TL
<ArrowRight data-icon="inline-end" aria-hidden />
</Link>
@@ -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"
>
<Link href="/paket">
<Link href="/paket?kaynak=kilit">
Kişisel analizi aç
<ArrowRight data-icon="inline-end" aria-hidden />
</Link>

View File

@@ -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<string | null>(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<HTMLElement>("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 (
<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">
{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}
</h2>

View File

@@ -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);

View File

@@ -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<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
useEffect(() => {
try {

View File

@@ -30,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<string, string | number>;