diff --git a/src/app/listem/page.tsx b/src/app/listem/page.tsx index 8ac28d0..e3a8453 100644 --- a/src/app/listem/page.tsx +++ b/src/app/listem/page.tsx @@ -20,12 +20,18 @@ export const metadata: Metadata = { export default function ListemPage({ searchParams, }: { - searchParams: Promise<{ acildi?: string; uret?: string }>; + // kaynak: üretim denemesini hangi yüzeyin başlattığı (bkz. + // uretim-basladi-olayi.tsx). Doğrulaması okuyan tarafta, allowlist ile. + searchParams: Promise<{ acildi?: string; uret?: string; kaynak?: string }>; }) { return ( }> - {searchParams.then(({ acildi, uret }) => ( - + {searchParams.then(({ acildi, uret, kaynak }) => ( + ))} ); diff --git a/src/features/rapor/components/listem-icerik.tsx b/src/features/rapor/components/listem-icerik.tsx index df114cb..388a9c6 100644 --- a/src/features/rapor/components/listem-icerik.tsx +++ b/src/features/rapor/components/listem-icerik.tsx @@ -20,6 +20,7 @@ import type { Mesaj } from "./sohbet-client"; import { ListemGovde } from "./listem-govde"; import { ListemBosCta } from "./listem-bos-cta"; import { ListeUretici } from "./liste-uretici"; +import { UretimBasladiOlayi } from "./uretim-basladi-olayi"; const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", { maximumFractionDigits: 0, @@ -28,9 +29,13 @@ const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", { export async function ListemIcerik({ uret, acildi, + kaynak, }: { uret: boolean; acildi: boolean; + /** Üretim denemesini başlatan yüzey (`?kaynak=`); doğrulaması + * UretimBasladiOlayi'nın allowlist'inde. */ + kaynak?: string; }) { // Tercihleri girili olan (profil çerezi duran) ziyaretçi girişten dönünce // "Henüz listen yok" boşluğuyla karşılaşmasın: callback ?uret=1 taşır, @@ -39,7 +44,14 @@ export async function ListemIcerik({ const cerezProfili = cerezdenTercihProfili( (await cookies()).get(PROFIL_CEREZ_ADI)?.value, ); - const geriYol = uret || cerezProfili ? "/listem?uret=1" : "/listem"; + // `kaynak` giriş round-trip'inde korunur; yoksa girişten dönen herkes + // "bilinmiyor" okunurdu. `uret=1` HER ZAMAN ilk parametre kalmalı: + // giris-karti.tsx:31 funnel metnini `startsWith("/listem?uret=1")` ile + // seçiyor, sıra bozulursa "Seçimlerin hazır" metni kaybolur. + const geriYol = + uret || cerezProfili + ? `/listem?uret=1${kaynak ? `&kaynak=${encodeURIComponent(kaynak)}` : ""}` + : "/listem"; const session = await verifySession(geriYol); const [user, satir, sohbetGecmisi] = await Promise.all([ getCurrentUser(), @@ -71,11 +83,20 @@ export async function ListemIcerik({ {/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim burada koşar, animasyon ve hata/kredi durumları burada yaşar. */} {uret ? ( - + <> + {/* Denemenin paydası: üretim gövdesiyle kardeş, aynı koşulda + mount olur (bkz. uretim-basladi-olayi.tsx). */} + + + ) : ( bosDurum )} diff --git a/src/features/rapor/components/uretim-basladi-olayi.tsx b/src/features/rapor/components/uretim-basladi-olayi.tsx new file mode 100644 index 0000000..d3bdff4 --- /dev/null +++ b/src/features/rapor/components/uretim-basladi-olayi.tsx @@ -0,0 +1,70 @@ +"use client"; + +// Üretim denemesinin paydasını ölçen görünmez bileşen: /listem?uret=1 ile +// üretim gövdesi kurulduğunda `liste_uretim_basladi` atar. Bugüne kadar +// yalnız başarı (`liste_olusturuldu`) sayıldığı için "kaç kişi denedi, kaçı +// listeye ulaştı" sorusunun paydası yoktu. +// +// Olay bilerek üretimi başlatan `liste-uretici.tsx`'te değil, onun kardeşi +// olan bu dosyada duruyor (o dosya bu turda başka bir işin altında). İkisi de +// `listem-icerik.tsx`'te aynı `uret` dalında mount edildiğinden koşulları +// birebir aynı: biri mount oluyorsa diğeri de oluyor. + +import { useEffect, useRef } from "react"; +import { olay } from "@/lib/analitik"; +import type { TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler"; +import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil"; + +/** + * `?kaynak=` sözleşmesinin izinli değerleri. Değer URL'den, yani kullanıcının + * denetiminden geliyor: allowlist olmadan Rybbit'e keyfi dizgi yazılabilir + * (kardinalite patlaması, panele enjekte edilen metin). Listede olmayan her + * şey `"bilinmiyor"`a düşer. Yeni bir üretim yüzeyi eklerken değerini önce + * buraya ekle, sonra href'e koy. + */ +const IZINLI_KAYNAKLAR = new Set([ + "sihirbaz", + "hazir_cta", + "giris_donusu", + "bos_durum", + "odeme_sonrasi", + "bilinmiyor", +]); + +export function UretimBasladiOlayi({ + kaynak, + paketli, + sunucuProfil, +}: { + /** `?kaynak=` ham değeri; allowlist dışındaysa "bilinmiyor" raporlanır. */ + kaynak: string; + paketli: boolean; + /** Profil çerezinden SSR'da çözülen yedek profil (bkz. listem-icerik). */ + sunucuProfil: TercihProfili | null; +}) { + // Mount başına tek atış. StrictMode geliştirmede effect'i iki kez koşuyor; + // `liste-uretici.tsx`'teki `basladi` ref'iyle aynı desen. `olayBirKez` + // BİLEREK kullanılmıyor: o oturum başına tekilleştirir, oysa aynı oturumda + // ikinci deneme de paydaya girmeli. + const atildi = useRef(false); + + useEffect(() => { + if (atildi.current) return; + atildi.current = true; + const kaynakGuvenli = IZINLI_KAYNAKLAR.has(kaynak) ? kaynak : "bilinmiyor"; + olay("liste_uretim_basladi", { + kaynak: kaynakGuvenli, + paketli: paketli ? "evet" : "hayir", + // Üretimin gerçekten başlayabilmesinin koşulu: bu ifade + // `liste-uretici.tsx`'teki `tercihProfiliOku() ?? sunucuProfil` + // ifadesinin ikizidir — ikisi ayrı dosyada olduğu için elle eşit + // tutulmalı. O dosya açıldığında (bkz. backlog: olayın gerçek çağrı + // yerine taşınması) tek yere alınacak. Ham sıralama/seçim GÖNDERİLMEZ + // (bkz. lib/analitik.ts kuralı) — yalnız profilin varlığı. + profil: (tercihProfiliOku() ?? sunucuProfil) ? "var" : "yok", + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} diff --git a/src/lib/analitik.ts b/src/lib/analitik.ts index 186ff70..147da7c 100644 --- a/src/lib/analitik.ts +++ b/src/lib/analitik.ts @@ -17,6 +17,12 @@ export type OlayAdi = | "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"