/
pvldchkv
/
unity
Обзор
Документация
Войти
/
pvldchkv
/
unity
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/clickstream/ClickstreamContext.tsx
189 строк
6 KB
Dyachkov Pavel
Add clickstream analytics infrastructure and mark up pages
29 апр 2026, 16:07
29 апр 2026, 16:07
9e448ec
Код
Авторство
О чём код?
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { DEFAULT_APPLICATION, STORAGE_KEY_SHOW_EVENTS, TOAST_DURATION_MS, TOAST_STACK_LIMIT, } from "./config"; import { EventsToastStack, type ClickstreamToastItem, } from "./EventsToastStack"; import type { ClickstreamEvent, ClickstreamKind, ClickstreamScope, TrackInput, } from "./types"; import { useAuth } from "../projects/unity-investor/context/AuthContext"; const NOOP_TRACK: TrackApi = { stage: () => {}, event: () => {}, }; interface TrackApi { // Жизненный цикл экрана / процесса (открытие страницы и т.п.). // Соответствует analytics.stage(...) в Сбер-кликстриме. stage: (input: TrackInput) => void; // Действия пользователя (клики и т.п.). // Соответствует analytics.event(...) в Сбер-кликстриме. event: (input: TrackInput) => void; } interface ClickstreamContextValue { // Флаг dev-режима: показывать ли тосты при срабатывании событий. showEvents: boolean; setShowEvents: (next: boolean) => void; track: TrackApi; } const ClickstreamContext = createContext<ClickstreamContextValue | null>(null); function readStoredFlag(): boolean { try { return localStorage.getItem(STORAGE_KEY_SHOW_EVENTS) === "true"; } catch { return false; } } function persistFlag(next: boolean) { try { localStorage.setItem(STORAGE_KEY_SHOW_EVENTS, String(next)); } catch { // ignore — приватный режим браузера и т.п. } } export function ClickstreamProvider({ children }: { children: ReactNode }) { const [showEvents, setShowEventsState] = useState<boolean>(() => readStoredFlag(), ); const [toasts, setToasts] = useState<ClickstreamToastItem[]>([]); // Зеркалим showEvents в ref, чтобы emit() читал свежее значение, // не пересоздаваясь при каждом тоггле и не инвалидируя useMemo для track. const showEventsRef = useRef(showEvents); useEffect(() => { showEventsRef.current = showEvents; }, [showEvents]); const idRef = useRef(0); const setShowEvents = useCallback((next: boolean) => { setShowEventsState(next); persistFlag(next); if (!next) { // При выключении сразу прячем все висящие тосты, // чтобы не оставались на экране. setToasts([]); } }, []); const dismissToast = useCallback((id: number) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const pushToast = useCallback( (kind: ClickstreamKind, event: ClickstreamEvent) => { idRef.current += 1; const id = idRef.current; setToasts((prev) => { const next = [...prev, { id, kind, event }]; return next.length > TOAST_STACK_LIMIT ? next.slice(-TOAST_STACK_LIMIT) : next; }); window.setTimeout(() => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, TOAST_DURATION_MS); }, [], ); const emit = useCallback( (kind: ClickstreamKind, input: TrackInput) => { const event: ClickstreamEvent = { application: input.application ?? DEFAULT_APPLICATION, label: input.label, ...(input.properties ? { properties: input.properties } : {}), }; // Лог в консоль — всегда, чтобы можно было снять «отчёт» через DevTools // независимо от тумблера. Метку ставим осмысленную, чтобы фильтровать. console.debug(`[clickstream:${kind}]`, event); // Здесь же в будущем будет реальная отправка в clickstream-сервис, // когда подключим — независимо от showEvents. if (showEventsRef.current) { pushToast(kind, event); } }, [pushToast], ); const track = useMemo<TrackApi>( () => ({ stage: (input) => emit("stage", input), event: (input) => emit("event", input), }), [emit], ); const value = useMemo<ClickstreamContextValue>( () => ({ showEvents, setShowEvents, track }), [showEvents, setShowEvents, track], ); return ( <ClickstreamContext.Provider value={value}> {children} <EventsToastStack toasts={toasts} onDismiss={dismissToast} /> </ClickstreamContext.Provider> ); } export function useClickstream(): ClickstreamContextValue { const ctx = useContext(ClickstreamContext); if (!ctx) { throw new Error( "useClickstream must be used within ClickstreamProvider", ); } return ctx; } // Удобный хук для компонентов, которым нужно только звать track(). // // Можно передать `scope`, чтобы зафиксировать общие поля для всей страницы: // const track = useTrack({ application: "лендинг" }); // track.event({ label: "Вход/Войти_Клик" }); // // → application автоматически проставится в "лендинг" // // Локальный вызов track.event(...) с явным `application` имеет приоритет над scope. // // Для роли «модератор» возвращается no-op API: события для модератора // не отправляются (его нет в дереве метрик SberUnity, и его взаимодействия // с прототипом не должны попадать в боевую аналитику). export function useTrack(scope?: ClickstreamScope): TrackApi { const { track } = useClickstream(); const { role } = useAuth(); return useMemo(() => { if (role === "moderator") return NOOP_TRACK; if (!scope?.application) return track; const application = scope.application; return { stage: (input) => track.stage({ application, ...input }), event: (input) => track.event({ application, ...input }), }; }, [track, role, scope?.application]); }