IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA18/32-dars23 daqiqa
Mundarija (34)

Analitika React'da: sahifa ko'rishlari, hodisalar, rozilik va maxfiylik

Qisqacha: Analitika — qaysi sahifa ochilgani va qaysi tugma bosilganini sanash. SPA'da sahifa qayta yuklanmaydi, shuning uchun "sahifa ko'rildi" ni router o'zgarganda o'zingiz yuborasiz. Hodisalar navbatda yig'ilib, sahifa yashirilganda navigator.sendBeacon bilan ketadi. Tayyor xizmatlar — GA4, Yandex Metrika, PostHog, Plausible, Umami — tashqi skript, hisob va kalit talab qiladi; posthog-js (+98.3 KB) vazifalar ning Trusted Types siyosati ostida ochilmadi. Eng muhimi — maxfiylik: shaxsiy ma'lumot yubormang, yo'lni umumlashtiring (/vazifa/7 → /vazifa/:id), GPC/DNT ni hurmat qiling. Cookie yoki identifikator ishlatsangiz — Yevropada oldindan rozilik (opt-in) shart. O'zbekistonda shaxsga doir ma'lumot uchun rozilik va mahalliy saqlash talablari bor.

Bu darsda

  • SPA'da sahifa ko'rishlarini router bilan to'g'ri sanaysiz va yo'lni umumlashtirasiz.
  • Hodisalarni navbatga yig'ib, sendBeacon bilan yuborasiz; UTM belgilarini o'qiysiz.
  • GA4, Yandex Metrika, PostHog, Plausible va Umami'ni hajm, CSP va maxfiylik bo'yicha solishtirasiz.
  • Rozilik oynasini qorong'u naqshlarsiz yasaysiz, GPC va Do Not Track'ni hurmat qilasiz.
  • Qachon oldindan rozilik (opt-in) shartligini GDPR va O'zbekiston qonuni bo'yicha tushuntirasiz.

Oldin bilishingiz kerak: React Router I, Performance API va Core Web Vitals, Cookie'lar JS'dan, localStorage va sessionStorage, Brauzer xavfsizlik modeli.

1. Nega bu kerak?

Oldingi darsda faylni foydalanuvchiga berdik. Endi foydalanuvchidan nimadir olamiz — u ilovadan qanday foydalanishi haqida ma'lumot. Jasur akaning savollari:

  • Qaysi taom sahifasi eng ko'p ochiladi?
  • «Buyurtma berish» ni bosganlardan nechtasi to'lovgacha yetadi?
  • Instagram'dagi reklama ko'proq odam olib keldimi yoki Telegram kanal?

Taxmin bilan javob berish mumkin, lekin qaror pul turadi. Analitika (analytics) — foydalanuvchilar harakatini sanash va jamlash: qancha, qayerdan, qaysi yo'l bilan. Xuddi do'kon eshigidagi hisoblagich kabi: kim kirganini emas, nechta odam kirganini sanaydi.

O'xshatishdagi farq muhim. Hisoblagich odamning yuzini suratga olmaydi. Analitika ham shunday bo'lishi kerak: "nechta", "qaysi sahifa", "qaysi tugma" — ha; "kim", "nima yozdi" — yo'q. Bu darsning yarmi — texnika, yarmi — shu chegarani ushlab turish.

2. SPA'da sahifa ko'rishlari

2.1 Muammo: sahifa qayta yuklanmaydi

Oddiy saytda har havola — yangi sahifa: analitika skripti har safar qayta ishga tushadi va "sahifa ko'rildi" yuboradi. SPA'da esa (SPA va client-side routing) skript faqat bir marta yuklanadi. Keyingi o'tishlarni router history.pushState bilan qiladi — brauzer uchun bu yangi sahifa emas. Tashqi skript buni o'zi bilmasa, analitikada butun tashrif — bitta sahifa.

Yechim: router o'zgarganda o'zingiz "sahifa ko'rildi" yuborasiz. React'da eng qisqa yo'l — useLocation va effekt. Havolalarni bosib ko'ring:

jsx
import { useEffect } from "react";
import {
  createMemoryRouter,
  RouterProvider,
  Link,
  Outlet,
  useLocation,
} from "react-router";

function generalizePath(path) {
  return path.replace(/\/\d+(?=\/|$)/g, "/:id") || "/";
}

function track(event) {
  console.log("yuborildi:", JSON.stringify(event));
}

function PageViews() {
  const { pathname } = useLocation();
  useEffect(() => {
    track({ type: "sahifa", path: generalizePath(pathname) });
  }, [pathname]);
  return null;
}

function Layout() {
  return (
    <>
      <PageViews />
      <nav>
        <Link to="/">Bosh sahifa</Link>{" "}
        <Link to="/vazifa/7">Vazifa 7</Link>{" "}
        <Link to="/statistika">Statistika</Link>
      </nav>
      <Outlet />
    </>
  );
}

const router = createMemoryRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      { index: true, element: <p>Ro'yxat</p> },
      { path: "vazifa/:id", element: <p>Vazifa</p> },
      { path: "statistika", element: <p>Statistika sahifasi</p> },
    ],
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Konsolda:

text
yuborildi: {"type":"sahifa","path":"/"}
yuborildi: {"type":"sahifa","path":"/vazifa/:id"}
yuborildi: {"type":"sahifa","path":"/statistika"}
yuborildi: {"type":"sahifa","path":"/"}

Natija oynasida manzil satri yo'q, shuning uchun createMemoryRouter (React Router I darsidagi kabi). To'rtta o'tish — to'rtta "sahifa". Ikkinchi qatorga qarang: /vazifa/7 emas, /vazifa/:id.

2.2 Nega yo'l umumlashtiriladi

generalizePath raqamli bo'laklarni :id ga aylantiradi. Ikki sabab:

  1. Foydali hisobot. Jasur akaga "vazifa sahifasi 340 marta ochildi" kerak, "/vazifa/1 — 3, /vazifa/2 — 1, …" degan 300 qatorli ro'yxat emas.
  2. Maxfiylik. Kim qaysi vazifani (buyurtmani, bemor kartasini, foydalanuvchi profilini) ochgani — shaxsga doir ma'lumotga juda yaqin. Analitikaga u kerak emas — demak, yuborilmaydi.

Shu sababli so'rov qatori (?filtr=…&q=…) va # ham tashlanadi: qidiruv matnida ism yoki telefon bo'lishi mumkin. vazifalar kanonining maxfiylik.ts dagi funksiyalari (kanon kodi):

ts
function yolniUmumlashtir(yol: string): string {
  const [faqatYol = "/"] = yol.split(/[?#]/);
  return faqatYol.replace(/\/\d+(?=\/|$)/g, "/:id") || "/";
}

function kuzatmaslikSoralgan(nav: {
  globalPrivacyControl?: boolean;
  doNotTrack?: string | null;
}): boolean {
  return nav.globalPrivacyControl === true || nav.doNotTrack === "1";
}

console.log(yolniUmumlashtir("/vazifa/12/tahrir"));
console.log(yolniUmumlashtir("/?filtr=faol&q=non#royxat"));
console.log(kuzatmaslikSoralgan({ globalPrivacyControl: true }));
console.log(kuzatmaslikSoralgan({ doNotTrack: null }));

Konsolda:

text
/vazifa/:id/tahrir
/
true
false

(?=\/|$) — oldinga qarash (Lookahead va lookbehind): raqamdan keyin / yoki yo'l oxiri bo'lsin. Shuning uchun /v2/menyu dagi 2 tegilmaydi — u alohida bo'lak emas. kuzatmaslikSoralgan haqida — «Rozilik» bo'limida.

2.3 Kanon nega effekt emas, router.subscribe ishlatdi

PageViews komponenti qulay, lekin bitta tuzog'i bor: dev rejimidagi StrictMode effektni ikki marta ishga tushiradi — birinchi sahifa ikki marta sanaladi. Kanon sahifa ko'rishlarini React'dan tashqarida, router holatiga obuna bo'lib yuboradi (main.tsx, kanon kodi):

tsx
// 21/#18: sahifa ko'rishlari — router holatidan (navigatsiya
// tugaganda, yo'l o'zgargan bo'lsa). Basename'siz yo'l: til
// alohida maydonda
let oxirgiYol = router.state.location.pathname;
sahifaKorildi(oxirgiYol);
router.subscribe((holat) => {
  if (
    holat.navigation.state === "idle" &&
    holat.location.pathname !== oxirgiYol
  ) {
    oxirgiYol = holat.location.pathname;
    sahifaKorildi(oxirgiYol);
  }
});
  • navigation.state === "idle" — loader'li sahifa ma'lumoti yuklanib bo'lgandan keyin. Yuklanayotgan (hali ko'rinmagan) sahifa sanalmaydi.
  • pathname !== oxirgiYol — faqat yo'l o'zgarsa. Filtr o'zgarishi (?filtr=bajarilgan) yangi sahifa emas.
  • Basename'siz — /ru/vazifa/2 emas, /vazifa/2: til alohida maydonda (til: document.documentElement.lang). Bitta sahifa to'rt tilda to'rtta sahifa bo'lib ketmaydi.

3. Hodisalar va yuborish

3.1 Hodisa — fe'l, matnsiz

Sahifa ko'rishdan tashqari, mahsulot uchun muhim hodisalar (custom events) sanaladi: "vazifa qo'shildi", "CSV eksport qilindi", "buyurtma berildi". Kanon ularni snake_case nomlar bilan yuboradi va hech qanday tafsilot qo'shmaydi:

tsx
    hodisa("vazifa_qoshildi"); // 21/#18: matnsiz — faqat "nima bo'ldi"

Vazifa matni («Shifokorga yozilish — +998 90 …») yuborilmaydi. Analitikaga "nechta vazifa qo'shildi" kerak, "qanday vazifa" emas.

3.2 Navbat va sendBeacon

Har hodisani darhol yuborish — har bosishda tarmoq so'rovi. Kanon hodisalarni navbatga yig'adi va ikki holatda yuboradi: 10 tasi to'planganda yoki sahifa yashirilganda (boshqa tabga o'tildi, telefon qulflandi, sahifa yopilmoqda). Navbat g'oyasi — kichik model (chegara 3, yuborish — konsolga):

jsx
import { useRef, useState } from "react";

function createTracker(send, limit = 3) {
  let queue = [];
  return {
    event(name) {
      queue.push({ name, time: Date.now() });
      if (queue.length >= limit) this.flush();
    },
    flush() {
      if (queue.length === 0) return;
      send(queue);
      queue = [];
    },
  };
}

export default function App() {
  const [sent, setSent] = useState(0);
  const tracker = useRef(null);
  if (tracker.current === null) {
    tracker.current = createTracker((batch) => {
      console.log("yuborildi:", batch.map((e) => e.name).join(", "));
      setSent((n) => n + batch.length);
    });
  }
  const track = (name) => tracker.current.event(name);
  return (
    <>
      <button onClick={() => track("vazifa_qoshildi")}>
        Vazifa qo'shish
      </button>{" "}
      <button onClick={() => track("eksport_csv")}>
        CSV eksport
      </button>
      <p>Yuborilgan hodisalar: {sent}</p>
    </>
  );
}

Konsolda:

text
yuborildi: vazifa_qoshildi, vazifa_qoshildi, eksport_csv

Uch hodisa — bitta so'rov. tracker.current === null tekshiruvi — obyekt bir marta yaratiladi (useRef I darsidagi "dangasa boshlash" naqshi), har renderda yangisi emas.

Haqiqiy yuborish — navigator.sendBeacon(url, blob). Bu POST so'rov, lekin ikki xususiyati bor:

  • Sahifa yopilayotganda ham ketadi. Oddiy fetch sahifa yopilsa uzilishi mumkin. Beacon esa brauzer navbatiga qo'yiladi va sahifa yopilgandan keyin ham yuboriladi.
  • Javob yo'q. Natija kerak emas — faqat true/false ("navbatga qo'yildimi"). Hajmi cheklangan (bir necha o'n KB) — katta ma'lumot uchun emas.

Qachon yuborish kerak? unload va beforeunload hodisalari telefonda ishonchsiz (ko'pincha kelmaydi). To'g'ri hodisa — visibilitychange va document.visibilityState === "hidden". Kanon tekshiruvi bu yerda haqiqiy xato topdi: tinglovchi avval window ga qo'yilgan edi. visibilitychange esa document da keladi — window dagi tinglovchi hech qachon ishlamadi, navbat ketmay qolardi. Tuzatilgan kod (kanon):

ts
if (MANZIL !== undefined) {
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
      yubor();
    }
  });
}

3.3 UTM: odam qayerdan keldi

Instagram reklamasi yoki Telegram post'idagi havolaga UTM belgilar qo'shiladi: ?utm_source=telegram&utm_medium=post&utm_campaign=kuz_menyu. Ular analitikaga "bu tashrif qayerdan" deydi. Ilova ularni birinchi sahifada o'qiydi va manzildan olib tashlaydi — foydalanuvchi havolani do'stiga ulashsa, do'st ham "Telegram'dan keldi" bo'lib sanalmasin. Bu — 2-mashq.

Tekshirib ko'ring: Nega navbatni yuborish uchun visibilitychange tanlangan, beforeunload emas?

Javob

Telefonda foydalanuvchi ko'pincha sahifani "yopmaydi" — boshqa ilovaga o'tadi, keyin tizim tabni xotiradan o'chiradi. Bunda beforeunload ham, unload ham kelmaydi. visibilitychange (hidden) esa sahifa ekrandan ketgan zahoti keladi — bu yuborish uchun oxirgi ishonchli imkoniyat. Bundan tashqari, beforeunload tinglovchisi brauzerning "orqaga-oldinga keshi" ni (bfcache) buzishi mumkin.

4. Tayyor xizmatlar

4.1 Beshta mashhur yo'l

O'z analitikangiz — sanash va yuborish. Hisobot, grafiklar, "voronka" (funnel), foydalanuvchi yo'llari esa ancha katta ish. Shuning uchun ko'pchilik tayyor xizmat ishlatadi:

Xizmat Kim saqlaydi Cookie/identifikator
Google Analytics 4 (GA4) Google serverlari ha (_ga)
Yandex Metrika Yandex serverlari ha; Vebvizor — sessiya yozuvi
PostHog bulut yoki o'z serveringiz ha (sukut); replay, flag'lar
Plausible bulut (EI) yoki o'z serveringiz yo'q
Umami o'z serveringiz (ochiq kod) yo'q

Yandex Metrika O'zbekiston va MDH saytlarida keng tarqalgan. Uning Vebvizor funksiyasi foydalanuvchi sahifada nima qilganini videoga o'xshab yozib oladi — sichqoncha, bosishlar, ba'zan kiritilgan matn ham. Bu kuchli vosita, lekin maxfiylik nuqtai nazaridan eng og'iri. Plausible va Umami — "cookie'siz" analitika: kunlik aylanadigan tasodifiy qiymatlar bilan unik tashrifni taxminlaydi, shaxsni kuzatmaydi.

Biz bu xizmatlarda hisob ochmadik va kalit yaratmadik. Quyidagi kodlar hujjatlar bo'yicha yozilgan, ishga tushirilmagan.

4.2 SPA uchun ulanish kodi

GA4 (gtag.js). Sahifa ko'rishni o'zingiz yuborasiz — aks holda GA4 ning "brauzer tarixi o'zgarishi" bo'yicha avtomatik sanashi bilan ikki marta sanaladi:

js
// index.html'da: <script async src="https://www.googletagmanager.com
//   /gtag/js?id=G-XXXXXXX"> (bir qatorda)
window.dataLayer = window.dataLayer || [];
function gtag() {
  dataLayer.push(arguments);
}
gtag("consent", "default", {
  analytics_storage: "denied", // rozilikkacha cookie yo'q
  ad_storage: "denied",
  ad_user_data: "denied", // Consent Mode v2
  ad_personalization: "denied",
});
gtag("js", new Date());
gtag("config", "G-XXXXXXX", { send_page_view: false });

// router o'zgarganda (GA4 parametri — to'liq manzil):
gtag("event", "page_view", {
  page_location: location.origin + generalizePath(pathname),
});
// rozilik berilganda:
gtag("consent", "update", { analytics_storage: "granted" });

consent buyruqlari — Google'ning Consent Mode rejimi: rozilikkacha GA4 cookie yozmaydi. 2024-yildan beri u ikkinchi versiyada: hujjatga ko'ra to'rtta kalit — analytics_storage, ad_storage, ad_user_data, ad_personalization. G-XXXXXXX — sizning o'lchov identifikatoringiz.

Ikki tuzoq. Birinchisi — parametr nomi: GA4 da sahifa manzili page_location (to'liq URL). Eski maqolalardagi page_path — oldingi Universal Analytics'dan qolgan. Ikkinchisi — send_page_view: false faqat birinchi avtomatik sahifa ko'rishni o'chiradi. GA4 sozlamalaridagi "Enhanced measurement → brauzer tarixi o'zgarishlari" bandi yoqiq qolsa, router o'tishlari baribir ikki marta sanaladi — uni GA4 panelida o'chirasiz.

Yandex Metrika:

js
ym(XXXXXXXX, "init", {
  defer: true,
  clickmap: true,
  webvisor: false,
});
// router o'zgarganda — "hit":
ym(XXXXXXXX, "hit", generalizePath(pathname));
// maqsad (hodisa):
ym(XXXXXXXX, "reachGoal", "vazifa_qoshildi");

defer: true — birinchi sahifani avtomatik sanamaslik (SPA'da o'zingiz hit yuborasiz). webvisor: false — sessiya yozuvini ataylab o'chirdik.

PostHog (React):

jsx
import posthog from "posthog-js";

posthog.init("phc_XXXXXXXX", {
  api_host: "https://eu.i.posthog.com",
  capture_pageview: false, // router bilan o'zingiz
  disable_session_recording: true,
  persistence: "memory", // cookie'siz rejim
});
posthog.capture("$pageview", {
  $current_url: generalizePath(pathname),
});
posthog.capture("vazifa_qoshildi");

Plausible — bitta skript teg, maxsus hodisa: plausible("Vazifa qo'shildi"). SPA'dagi tarix o'zgarishini skript o'zi sanaydi.

4.3 Biz sinab ko'rdik: posthog-js

Kanon posthog-js 1.438.7 ni soxta kalit bilan o'lchadi — api_host o'z origin'imizga yo'naltirilgan, ya'ni hech qayerga ketmaydi. Natija (kutubxonalar-21.txt):

text
analitika-posthog      JS  168423 (+  98.3 KB) CSS      0 | CSP require-trusted-types-for×1 | xato: This document requires 'TrustedScriptURL' assignment. The action has been blocked. | ochiq: tashqi yo'q

Ikki xulosa:

  • +98.3 KB gzip — vazifalar ning butun birinchi yuklashining deyarli yarmi. Analitika uchun bu juda qimmat: u har bir sahifada, eng birinchi yuklanadi.
  • TrustedScriptURL — kutubxona qo'shimcha skriptlarni (masalan, qo'shimcha modullarni) <script src> bilan yuklashga urindi va Trusted Types siyosati to'sdi. GA4 va Metrika ham asosiy skriptni tashqi domendan yuklaydi: CSP'ga script-src https://www.googletagmanager.com yoki https://mc.yandex.ru va connect-src qo'shiladi. Bu — har ikki domenga sahifangiz ustidan to'liq ishonch berish.

Kanon qarori: tashqi xizmat yo'q, o'z yengil analitikasi — +0.5 KB.

4.4 Reklama blokerlari

Ko'p foydalanuvchilarda reklama blokerlari bor (uBlock Origin, brauzerlarning o'z himoyasi). Ular GA4, Metrika va PostHog domenlarini to'sadi — bu foydalanuvchilar analitikada umuman ko'rinmaydi. Ba'zi jamoalar so'rovlarni o'z domeni orqali "proksi" qiladi (/ingest/…). Texnik jihatdan bu ishlaydi. Lekin bloker — foydalanuvchining tanlovi. Uni aylanib o'tishdan oldin o'ylang: maqsad — mahsulotni yaxshilash, foydalanuvchini aldash emas. Raqamlar to'liq bo'lmasligini qabul qilib, trendga qarang.

5. Rozilik va qonunchilik

5.1 Qachon oldindan rozilik shart

Analitika — foydalanuvchi haqida ma'lumot yig'ish. Qonun buni ikki savol bilan o'lchaydi: shaxsga doir ma'lumot yig'ilyaptimi va qurilmada narsa saqlanyaptimi (cookie, localStorage dagi identifikator)?

flowchart TB
  A{"Cookie yoki identifikator bormi?"} -- ha --> O["Oldindan rozilik (opt-in)"]
  A -- yo'q --> B{"Shaxsga doir ma'lumot? (IP, email, ID)"}
  B -- ha --> O
  B -- yo'q --> C{"Tashqi xizmatmi? (GA4, Metrika)"}
  C -- ha --> O
  C -- yo'q --> D["Shaxssiz analitika: o'chirish + GPC/DNT"]

Diagrammaga qarang. Yevropa qoidalari (GDPR va ePrivacy direktivasi): qurilmaga "qat'iy zarur bo'lmagan" narsani yozish yoki o'qish uchun — oldindan, aniq, erkin rozilik (opt-in). Oldindan belgilangan katakcha rozilik emas (Yevropa Ittifoqi sudi, 2019, Planet49 ishi). Rozilikni qaytarib olish berish kabi oson bo'lishi kerak. Ba'zi Yevropa nazoratchilari (masalan, Fransiyaning CNIL'i) qat'iy cheklangan, shaxssiz statistika uchun istisno qiladi — shartlari bilan.

O'zbekiston: "Shaxsga doir ma'lumotlar to'g'risida"gi Qonun (O'RQ-547, 2019). Asosiy talablar:

  • shaxsga doir ma'lumotni qayta ishlash uchun — subyektning roziligi (qonunda sanab o'tilgan istisnolardan tashqari);
  • 2021-yilda qo'shilgan 27¹-modda (O'RQ-666, 2021-yil aprelidan kuchda): O'zbekiston fuqarolarining shaxsga doir ma'lumotlari internet orqali yig'ilsa, ular O'zbekiston hududidagi va davlat reyestrida ro'yxatdan o'tgan bazalarda saqlanishi kerak.

Ikkinchi band analitika uchun muhim: GA4 yoki Metrika ma'lumotni xorijdagi serverlarda saqlaydi. Agar u ma'lumot shaxsni aniqlashga imkon bersa, talab bilan to'qnashishi mumkin. Bu bo'lim — huquqiy maslahat emas. Qonunning amaldagi tahririni lex.uz'da o'qing, jiddiy loyihada yurist bilan maslahatlashing. Batafsil — Shaxsiy ma'lumotlar qonunchiligi darsida (31-qism).

5.2 Kanon qarori: sukut bo'yicha yoqilgan — sharti bilan

vazifalar da analitika sukut bo'yicha yoqilgan (opt-out), lekin buning uchun hamma shartlar bajarilgan (kanon qarori):

  • cookie yo'q, foydalanuvchi identifikatori yo'q, IP saqlanmaydi;
  • yuboriladi faqat: umumlashtirilgan yo'l, til, hodisa nomi, vaqt;
  • GPC yoki DNT bo'lsa — hech narsa yuborilmaydi;
  • Sozlamalarda bir bosishda o'chiriladi;
  • production'da yuborish manzili yo'q — dev server'dagi qabul qiluvchidan boshqa hech qayerga ketmaydi.

Bulardan birortasi o'zgarsa — masalan, tashqi xizmat qo'shilsa yoki foydalanuvchi ID'si paydo bo'lsa — rozilik oldindan so'ralishi kerak.

5.3 GPC va Do Not Track

Ba'zi brauzerlar va kengaytmalar har saytga "meni kuzatma" signalini yuboradi:

  • GPC (Global Privacy Control) — navigator.globalPrivacyControl === true va Sec-GPC: 1 sarlavhasi. AQShning bir qator shtatlarida (masalan, Kaliforniya va Kolorado) qonuniy kuchga ega signal.
  • Do Not Track — eski signal, navigator.doNotTrack === "1". Rasman eskirgan, lekin ba'zi brauzerlar hali yuboradi.

Kanon ikkalasini ham hurmat qiladi (kuzatmaslikSoralgan): bunday foydalanuvchining Sozlamalaridagi katakcha o'chiq va o'zgartirib bo'lmaydi, yonida sababi yozilgan. Qonun talab qilmasa ham, bu — foydalanuvchi aniq aytgan istak.

5.4 Rozilik oynasi

Agar rozilik (consent) kerak bo'lsa, uni halol so'rang. Asosiy qoidalar:

  • Tanlovdan oldin analitika skripti yuklanmaydi, cookie yozilmaydi.
  • «Rad etish» «Qabul qilish» kabi ko'rinadi va bir bosishda. Rad etishni kichik kulrang havolaga yashirish — qorong'u naqsh (dark pattern).
  • Tanlovni keyin o'zgartirish mumkin (Sozlamalarda yoki pastki havolada).
  • Matn aniq: nima yig'iladi, kim saqlaydi.
jsx
import { useState } from "react";

const KEY = "rozilik";

function readConsent() {
  try {
    return localStorage.getItem(KEY);
  } catch {
    return null; // saqlab bo'lmasa — so'raymiz
  }
}

export default function App() {
  const [consent, setConsent] = useState(readConsent);

  function choose(value) {
    localStorage.setItem(KEY, value);
    setConsent(value);
    const state = value === "ha" ? "yoqildi" : "o'chiq";
    console.log("tanlov:", value, "| analitika", state);
  }

  if (consent === null) {
    return (
      <section aria-label="Maxfiylik">
        <p>
          Analitika uchun ruxsat berasizmi? Faqat sahifa turi va
          hodisa nomi, cookie'siz.
        </p>
        <button onClick={() => choose("ha")}>
          Ruxsat beraman
        </button>{" "}
        <button onClick={() => choose("yoq")}>Faqat zarurlari</button>
      </section>
    );
  }
  return (
    <p>
      Analitika: {consent === "ha" ? "yoqilgan" : "o'chiq"}{" "}
      <button
        onClick={() => {
          localStorage.removeItem(KEY);
          setConsent(null);
        }}
      >
        O'zgartirish
      </button>
    </p>
  );
}

Konsolda:

text
tanlov: yoq | analitika o'chiq

Ekranda:

text
Analitika: o'chiq O'zgartirish

Ikki tugma bir xil <button> — teng ko'rinishda. Tanlov localStorage da saqlanadi — bu "qat'iy zarur" saqlash: foydalanuvchining o'z tanlovi, uni kuzatish uchun emas. readConsent dagi try — ba'zi brauzer rejimlarida localStorage xato tashlaydi (localStorage va sessionStorage). Natija oynasidagi localStorage vaqtinchalik — sahifa yangilansa tozalanadi.

Tekshirib ko'ring: Rozilik oynasi bor, lekin GA4 skripti index.html da <script async> bilan turibdi va rozilikdan oldin yuklanadi. Bu to'g'rimi?

Javob

Consent Mode'siz — yo'q: skript yuklanishi bilan cookie yozadi va ma'lumot yuboradi, rozilik oynasi esa faqat bezak bo'lib qoladi. Ikki to'g'ri yo'l bor. Birinchisi — skriptni faqat «Ruxsat beraman» dan keyin dinamik qo'shish. Ikkinchisi — Consent Mode bilan analytics_storage: "denied" ni skriptdan oldin e'lon qilish (yuqoridagi GA4 kodi). Ikkinchi holatda ham Google'ga cookie'siz signallar ketadi — Yevropada bu ham munozarali, shuning uchun ehtiyotkor jamoalar birinchi yo'lni tanlaydi.

6. Hujumchi nigohi: ma'lumot sizishi

Analitikadagi eng ko'p uchraydigan muammo — hujum emas, beixtiyor sizish. Uchinchi tomon xizmatiga yuborilgan ma'lumotni qaytarib bo'lmaydi.

  • URL'dagi sirlar. Parolni tiklash havolasi: /parol?token=8f3a…&email=ali@example.com. Analitika to'liq manzilni yuborsa — token va email Google yoki Yandex serverida. Kanon so'rov qatorini butunlay tashlaydi.
  • Sarlavhadagi ism. <title>Ali Valiyev — profil</title> — ko'p xizmatlar sahifa sarlavhasini ham yuboradi.
  • Hodisa tafsilotlari. track("qidiruv", { matn: query }) — qidiruvga odamlar telefon, manzil, kasallik nomini yozadi.
  • Sessiya yozuvi (Vebvizor, PostHog replay) — kiritish maydonlarini yashirish sozlamasi o'chiq bo'lsa, yozilgan hamma narsa ketadi.
  • Begona skript — to'liq kirish. Tashqi analitika skripti sahifangizdagi hamma narsani o'qiy oladi. Agar uning serveri buzilsa, zararli kod sizning sahifangizda ishlaydi. 2024-yilda mashhur polyfill.io domeni yangi egasi qo'liga o'tgach, uni ulagan ko'plab saytlarga zararli kod tarqatilgan. Bu mavzu Uchinchi tomon skriptlar va embedlar darsida davom etadi.

Himoya — "minimallashtirish": yubormaslik mumkin bo'lgan narsani yubormang. Kanon qoidasi: yo'l umumlashtiriladi, so'rov qatori tashlanadi, hodisa — faqat nom. Kelasi darsdagi xato kuzatuvida bunga matndagi email va telefonni yashiradigan shaxsiyniYashir qo'shiladi.

7. Vazifalar qadami: o'z yengil analitikasi (holat 18)

7.1 Fayllar

bash
git switch -c feat/analitika
Fayl Nima
src/kuzatuv/analitika.ts navbat, sendBeacon, sahifaKorildi, hodisa
src/kuzatuv/maxfiylik.ts yolniUmumlashtir, kuzatmaslikSoralgan (+ 19-dars uchun shaxsiyniYashir)
komponentlar/MaxfiylikSozlamasi.tsx «Anonim statistika» katakchasi
skriptlar/kuzatuv.js qabul qiluvchi — faqat dev server plagini
main.tsx, App.tsx router.subscribe; hodisa("vazifa_qoshildi"), hodisa("eksport_csv")
.env.development, vite-env.d.ts VITE_ANALITIKA_URL — faqat dev

Butun tizimning kaliti — manzil. analitika.ts boshidagi izoh (kanon):

ts
// kuzatuv/analitika.ts — o'z yengil analitikamiz (21/#18):
// sahifa ko'rishlari va bir nechta hodisa. Tashqi skript yo'q
// (GA4/Metrika/PostHog — o'lchandi: posthog-js +98.3 KB va
// Trusted Types ostida ochilmadi,
// natijalar/tajriba/kutubxonalar-21.txt), cookie yo'q,
// foydalanuvchi identifikatori yo'q. Yuboriladi:
// umumlashtirilgan yo'l, til, hodisa nomi, vaqt.
// Manzil — VITE_ANALITIKA_URL (dev: skriptlar/kuzatuv.js;
// production'da yo'q — funksiyalar hech narsa qilmaydi).

VITE_ANALITIKA_URL faqat .env.development da. Production build'da import.meta.env.VITE_ANALITIKA_URL — undefined va har funksiya birinchi qatorda qaytadi. Analitika serveri 25-qismda (HTTP, Express va REST API) yoziladi — o'shanda manzil production'ga qo'shiladi.

7.2 Navbatga qo'shish

ts
function qosh(
  hodisa: Omit<Hodisa, "vaqt" | "til" | "yol"> & { yol?: string },
): void {
  if (MANZIL === undefined || !analitikaYoqilganmi()) {
    return;
  }
  navbat.push({
    ...hodisa,
    yol: yolniUmumlashtir(hodisa.yol ?? location.pathname),
    til: document.documentElement.lang,
    vaqt: Date.now(),
  });
  if (navbat.length >= NAVBAT_CHEGARASI) {
    yubor();
  }
}

Tekshiruv har hodisada: foydalanuvchi Sozlamalarda o'chirsa, keyingi hodisa navbatga tushmaydi (o'chirilganda navbat ham bo'shatiladi). Omit<…> & { yol?: string } — chaqiruvchi faqat tur va nom beradi, qolganini funksiya o'zi to'ldiradi (Utility types).

7.3 Natija

Testlar: maxfiylik.test.js — 3 ta (yo'l, shaxsiy ma'lumot, GPC/DNT); node:test 233 → 236, Vitest 65. Dev server tekshiruvi (dev-21, haqiqiy Chrome va dev qabul qiluvchi):

text
✅ yuborildi (bitta beacon): sahifalar va hodisa: ["sahifa:/","hodisa:eksport_csv","sahifa:/statistika","sahifa:/sozlamalar"]
✅ yuborilganda vazifa matni yo'q; til bor; maydonlar: [false,true,["nom","til","tur","vaqt","yol"]]
✅ o'chirilgach — hech narsa yuborilmadi; localStorage: [0,"yoq"]
✅ CSP buzilishi yo'q: []
✅ GPC: katak o'chiq va faol emas, izoh, yuborilmadi: [[false,true],true,0]

Ikkinchi qator — maxfiylik isboti: yuborilgan ma'lumotda faqat beshta maydon, vazifa matni yo'q. Production build tekshiruvi (e2e-21, 55 ta):

text
✅ production: analitika so'rovi yo'q (VITE_ANALITIKA_URL yo'q): []
✅ build'da qabul qiluvchi manzili yo'q: false

Birinchi yuklash JS — 222.8 → 223.3 KB gzip (+0.5). axe — 0.

bash
git add -A
git commit

Commit (kanon): feat: o'z yengil analitika — sahifa va hodisa, shaxssiz, GPC; dev qabul qiluvchi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
butun tashrif — 1 sahifa SPA'da router o'zgarishi yuborilmaydi router bilan page_view/hit
har sahifa ikki marta avtomatik + qo'lda sanash; StrictMode effekt bittasini o'chiring; router.subscribe
300 ta /vazifa/N qatori yo'l umumlashtirilmagan /vazifa/:id
yopilganda hodisalar yo'qoladi unload/beforeunload, window da tinglash document + visibilitychange + sendBeacon
analitikada email va token to'liq URL yuborilgan so'rov qatorini tashlash
requires 'TrustedScriptURL' analitika skripti <script src> qo'shadi siyosat yoki o'z yechim
rozilikdan oldin cookie paydo bo'ldi skript index.html da rozilikdan keyin yuklash / Consent Mode
GPC foydalanuvchisi ham sanaldi signal tekshirilmagan globalPrivacyControl, doNotTrack

9. Mashqlar

Mashqlar kurs/mashqlar/21/18-analitika/ da. Yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Sahifa yopilayotganda ham ketadigan so'rov: navigator.
  • Navbatni yuborish uchun ishonchli hodisa:
  • /vazifa/12/tahrir umumlashtirilgandan keyin:
  • "Meni kuzatma" degan zamonaviy brauzer signali:
  • Yevropada cookie'li analitika uchun kerak bo'lgan rozilik turi:
Yechim
  • sendBeacon — brauzer navbatiga qo'yiladi, javob kutilmaydi.
  • visibilitychange — document da, visibilityState === "hidden" bo'lganda.
  • /vazifa/:id/tahrir — raqamli bo'lak :id ga.
  • GPC — navigator.globalPrivacyControl.
  • opt-in — oldindan, aniq rozilik; jim qolish rozilik emas.

2-mashq (o'rta): UTM belgilarini olish va tozalash

takeUtm(url) funksiyasini yozing: manzildan utm_ bilan boshlanadigan parametrlarni obyekt qilib olsin va ularsiz toza manzil qaytarsin. Boshqa parametrlar (?filtr=faol) joyida qolsin. Sinov: https://example.com/menyu?utm_source=telegram&filtr=faol&utm_campaign=kuz.

Ishora: new URL(url) va uning searchParams (URL va URLSearchParams); o'chirishdan oldin kalitlarni massivga oling — aylanish paytida o'chirish elementlarni o'tkazib yuboradi.

Yechim
js
function takeUtm(url) {
  const u = new URL(url);
  const utm = {};
  const keys = [...u.searchParams.keys()];
  for (const key of keys) {
    if (key.startsWith("utm_")) {
      utm[key] = u.searchParams.get(key);
      u.searchParams.delete(key);
    }
  }
  return { utm, clean: u.href };
}

const { utm, clean } = takeUtm(
  "https://example.com/menyu?utm_source=telegram" +
    "&filtr=faol&utm_campaign=kuz",
);
console.log(utm);
console.log(clean);

Konsolda:

text
{ utm_source: 'telegram', utm_campaign: 'kuz' }
https://example.com/menyu?filtr=faol

Brauzerda toza manzil history.replaceState(null, "", clean) bilan o'rnatiladi — sahifa qayta yuklanmaydi, tarixga yangi qadam qo'shilmaydi. UTM esa birinchi "sahifa ko'rildi" hodisasiga qo'shilib, bir marta yuboriladi.

3-mashq (qiyin): rozilikni kutadigan treker

createConsentTracker(send, { gpc }) yozing. Uch holat: "kutilmoqda", "ruxsat", "rad".

  1. Kutilayotganda hodisalar buferga yig'iladi (eng ko'pi 5 ta — eskilari tashlanadi).
  2. grant() — bufer bitta yuborishda ketadi, keyingi hodisalar darhol yuboriladi.
  3. deny() — bufer o'chiriladi, keyingi hodisalar e'tiborsiz.
  4. gpc: true bo'lsa — boshidan "rad", grant() ham uni o'zgartirmaydi.

Sinov: 7 ta hodisa → grant() → yana 1 ta; alohida — GPC'li treker.

Ishora: buferni cheklash — buffer.slice(-5) (Kesish, birlashtirish va matnga aylantirish).

Yechim
js
function createConsentTracker(send, { gpc = false } = {}) {
  let state = gpc ? "rad" : "kutilmoqda";
  let buffer = [];
  return {
    event(name) {
      if (state === "ruxsat") send([name]);
      else if (state === "kutilmoqda") {
        buffer = [...buffer, name].slice(-5);
      }
    },
    grant() {
      if (state !== "kutilmoqda") return;
      state = "ruxsat";
      if (buffer.length) send(buffer);
      buffer = [];
    },
    deny() {
      state = "rad";
      buffer = [];
    },
    get state() {
      return state;
    },
  };
}

const log = (batch) => console.log("yuborildi:", batch.join(", "));

const t = createConsentTracker(log);
for (let i = 1; i <= 7; i++) t.event(`h${i}`);
t.grant();
t.event("h8");
console.log(t.state);

const g = createConsentTracker(log, { gpc: true });
g.event("x");
g.grant();
g.event("y");
console.log(g.state);

Konsolda:

text
yuborildi: h3, h4, h5, h6, h7
yuborildi: h8
ruxsat
rad

Birinchi ikkita hodisa bufer chegarasidan chiqib ketdi. Bu ataylab: foydalanuvchi uzoq o'ylasa, xotira cheksiz o'smasin. GPC'li trekerdan hech narsa chiqmadi — grant() birinchi qatorda qaytdi. get state() — getter: tashqaridan o'qiladi, lekin t.state = "ruxsat" bilan o'zgartirib bo'lmaydi (Getter va setter).

4-mashq: Vazifalar qadami

feat/analitika branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: kuzatuv/ papkasi, MaxfiylikSozlamasi.tsx va to'rt tildagi matnlari, skriptlar/kuzatuv.js plagini, main.tsx dagi router.subscribe, ikkita hodisa(…) chaqiruvi, .env.development va vite-env.d.ts.

Yechim

Tekshiruv ro'yxati:

  • npm test — node 236, Vitest 65; npm run tip, lint, knip — toza.
  • npm run dev: bir nechta sahifani oching, CSV eksport qiling, boshqa tabga o'ting. Terminalda [analitika] {"tur":"sahifa","nom":"sahifa","yol":"/",…} qatorlari; http://localhost:5173/__kuzatuv — hammasi JSON'da. Vazifa matni yo'q.
  • Sozlamalar → «Anonim statistika…» katagini o'chiring → yana harakat qiling → yangi qator kelmaydi.
  • npm run build va npm run preview: Network'da analitika so'rovi yo'q; birinchi yuklash ~223.3 KB.
  • Brauzeringizda GPC'ni yoqing (masalan, kengaytma bilan) — katak o'chiq va faol emas, izohda sababi.

10. Real ishda

  • Mahsulot jamoalari — analitika qaror asosi: qaysi funksiya ishlatiladi, qayerda foydalanuvchi "yo'qoladi" (voronka). Product analyst va frontend dasturchi birga hodisalar ro'yxatini (tracking plan) yozadi.
  • O'zbekiston saytlari — Yandex Metrika va GA4 eng ko'p uchraydi; shaxsga doir ma'lumot va mahalliy saqlash talablari tufayli o'z serveridagi yechimlar (Umami, Plausible, PostHog self-host) ham tanlanadi.
  • Kod ko'rigida so'raladi: "bu hodisada shaxsiy ma'lumot bormi?", "SPA sahifa ko'rishlari ikki marta sanalmayaptimi?", "rozilikdan oldin nima yuklanadi?".
  • Intervyu savollari: "SPA'da page view qanday yuboriladi?", "sendBeacon nima va nega kerak?", "opt-in va opt-out farqi?", "analitikaga nimani yubormaslik kerak?".

Xulosa

  • SPA'da sahifa ko'rishi — router o'zgarganda o'zingiz; yo'l umumlashtiriladi (/vazifa/:id), so'rov qatori tashlanadi.
  • Hodisa — nom, matnsiz; navbat + document dagi visibilitychange + sendBeacon.
  • GA4, Metrika, PostHog — tashqi skript, cookie, CSP va TT muammolari (posthog +98.3 KB, TT to'sdi); Plausible/Umami — cookie'siz.
  • Cookie/identifikator yoki shaxsga doir ma'lumot — Yevropada oldindan rozilik; O'zbekistonda rozilik va mahalliy saqlash talabi (O'RQ-547).
  • GPC/DNT ni hurmat qiling; rozilik oynasida "Rad etish" — teng va oson.
  • Kanon: o'z analitikasi (+0.5 KB), shaxssiz, o'chirsa bo'ladi, production'da manzil yo'q.

Keyingi dars: Xatolarni kuzatish: Sentry frontendda — foydalanuvchida yuz bergan xatolarni yig'ish: window, Promise va React xatolari, reliz, iz va matndagi shaxsiy ma'lumotni yashirish.

Manbalar

  • MDN: Navigator.sendBeacon(), Document: visibilitychange event, Navigator.globalPrivacyControl, Navigator.doNotTrack
  • React Router: useLocation, router.subscribe — reactrouter.com
  • Google: "Measure single-page applications", "Consent mode" — developers.google.com/analytics
  • Yandex Metrika: "Counter initialization", hit, reachGoal — yandex.ru/support/metrica
  • PostHog: "JavaScript Web SDK" (capture_pageview, persistence) — posthog.com/docs; Plausible — plausible.io/docs; Umami — umami.is/docs
  • GDPR (Regulation (EU) 2016/679), ePrivacy Directive 2002/58/EC 5(3)-modda; CJEU C-673/17 (Planet49, 2019); CNIL: audience measurement exemption
  • O'zbekiston Respublikasining "Shaxsga doir ma'lumotlar to'g'risida"gi Qonuni (O'RQ-547, 2019-07-02) va 27¹-moddani qo'shgan O'RQ-666 (2021-01-14) — lex.uz
  • Global Privacy Control — globalprivacycontrol.org
  • Kanon: vazifalar holat 18, natijalar/tekshiruv-18.txt, brauzer-tekshiruv/natija-dev-18.txt, natija-21-18.txt, natijalar/tajriba/kutubxonalar-21.txt (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Analitika React'da: sahifa ko'rishlari, hodisalar, rozilik va maxfiylik — IlmHamroh