fix(analitik): görünüm hook'u callback ref + erken olay kuyruğu
useGorunumOlayi useRef + [ad, anahtarEki] deps'li effect kullanıyordu:
eleman ilk commit'te yoksa (kaydet-banner: mount sonrası açılır, portal
içinde) ya da sökülüp yeni düğümle geri geliyorsa (tadımlık satırı) gözlem
hiç kurulmuyordu → kaydet_banner_goruntulendi / tadimlik_goruntulendi = 0.
Hook artık callback ref döner; gözlem düğüm bağlanınca kurulur, sökülünce
React 19 ref temizliğiyle bırakılır. Çağıranlar yalnız ref={…} verdiği
için kullanım yerleri değişmedi.
olay(): Rybbit script'i afterInteractive; sert yüklemede mount anında
atılan olaylar window.rybbit henüz yokken sessizce düşüyordu. Artık en çok
20 olay, 500 ms aralıkla en çok 20 deneme (10 sn) kuyrukta bekler; script
gelmezse (reklam engelleyici, dev) kuyruk atılır.
olayBirKez(): hook'taki sessionStorage tekilleştirmesinin effect'ten
çağrılabilir hâli (anahtar biçimi aynı: kolaytercih.olay.<ad>[.<ek>]).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,54 +1,60 @@
|
|||||||
"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 {
|
|
||||||
if (window.sessionStorage.getItem(anahtar)) return;
|
|
||||||
} catch {
|
|
||||||
// Storage engelliyse tekilleştirmeden vazgeç, olay yine atılabilir
|
|
||||||
}
|
|
||||||
|
|
||||||
const io = new IntersectionObserver((entries) => {
|
|
||||||
if (!entries.some((e) => e.isIntersecting)) return;
|
|
||||||
io.disconnect();
|
|
||||||
try {
|
try {
|
||||||
window.sessionStorage.setItem(anahtar, "1");
|
if (window.sessionStorage.getItem(olayAnahtari(ad, anahtarEki))) return;
|
||||||
} catch {
|
} catch {
|
||||||
// yut
|
// Storage engelliyse tekilleştirmeden vazgeç, olay yine atılabilir
|
||||||
}
|
}
|
||||||
olay(ad, ozelliklerRef.current);
|
|
||||||
});
|
|
||||||
io.observe(el);
|
|
||||||
return () => io.disconnect();
|
|
||||||
}, [ad, anahtarEki]);
|
|
||||||
|
|
||||||
return ref;
|
const io = new IntersectionObserver((entries) => {
|
||||||
|
if (!entries.some((e) => e.isIntersecting)) return;
|
||||||
|
io.disconnect();
|
||||||
|
olayBirKez(ad, ozelliklerRef.current, anahtarEki);
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
[ad, anahtarEki],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -48,16 +49,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