diff --git a/app/resources/js/plugins/metrika.ts b/app/resources/js/plugins/metrika.ts new file mode 100644 index 00000000..3ba6559b --- /dev/null +++ b/app/resources/js/plugins/metrika.ts @@ -0,0 +1,59 @@ +/** + * Ленивый загрузчик Яндекс.Метрики для портала. + * Грузится ТОЛЬКО из роутера при входе в кабинет (meta.layout==='app') — см. router/index.ts. + * id счётчика читается из (проставляется welcome.blade.php по config). + * Вебвизор пишет с маскировкой ПДн (классы ym-hide-content в компонентах кабинета). + */ +declare global { + interface Window { + ym?: (...args: unknown[]) => void; + } +} + +let loaded = false; + +function counterId(): number | null { + const meta = document.querySelector('meta[name="metrika-id"]'); + const raw = meta?.getAttribute('content'); + const id = raw ? Number(raw) : NaN; + return Number.isFinite(id) && id > 0 ? id : null; +} + +/** Один раз инжектит tag.js и инициализирует Вебвизор. Идемпотентно. No-op без id. */ +export function ensureLoaded(): void { + if (loaded) return; + const id = counterId(); + if (id === null) return; + loaded = true; + + // стандартный bootstrap-стаб ym до загрузки tag.js + const w = window as Window & { ym?: { (...a: unknown[]): void; a?: unknown[]; l?: number } }; + w.ym = + w.ym || + function (...args: unknown[]) { + (w.ym as { a?: unknown[] }).a = (w.ym as { a?: unknown[] }).a || []; + (w.ym as { a: unknown[] }).a.push(args); + }; + (w.ym as { l?: number }).l = Number(new Date()); + + const s = document.createElement('script'); + s.async = true; + s.src = `https://mc.yandex.ru/metrika/tag.js?id=${id}`; + document.head.appendChild(s); + + // defer:true — без авто-первого-хита; первый хит шлёт роутер вручную + w.ym(id, 'init', { + webvisor: true, + clickmap: true, + trackLinks: true, + accurateTrackBounce: true, + defer: true, + }); +} + +/** Отправить заход (SPA-переход). No-op без id/ym. */ +export function hit(url: string): void { + const id = counterId(); + if (id === null || typeof window.ym !== 'function') return; + window.ym(id, 'hit', url); +} diff --git a/app/tests/Frontend/metrika.spec.ts b/app/tests/Frontend/metrika.spec.ts new file mode 100644 index 00000000..b1f47042 --- /dev/null +++ b/app/tests/Frontend/metrika.spec.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +/** + * Тесты ленивого загрузчика Яндекс.Метрики (resources/js/plugins/metrika.ts). + * Загрузчик грузится только из роутера при входе в кабинет (см. router/index.ts). + */ +describe('metrika loader', () => { + beforeEach(() => { + document.head.innerHTML = ''; + document.body.innerHTML = ''; + // @ts-expect-error чистим глобал между тестами + delete window.ym; + vi.resetModules(); + }); + + it('без meta-тега ничего не грузит и не падает', async () => { + const { ensureLoaded } = await import('../../resources/js/plugins/metrika'); + ensureLoaded(); + expect(document.querySelector('script[src*="mc.yandex.ru"]')).toBeNull(); + }); + + it('с meta-тегом инжектит tag.js один раз (идемпотентно)', async () => { + const meta = document.createElement('meta'); + meta.name = 'metrika-id'; + meta.content = '99999999'; + document.head.appendChild(meta); + + const { ensureLoaded } = await import('../../resources/js/plugins/metrika'); + ensureLoaded(); + ensureLoaded(); + const scripts = document.querySelectorAll('script[src*="mc.yandex.ru/metrika/tag.js"]'); + expect(scripts.length).toBe(1); + }); + + it('hit() вызывает window.ym с id из meta и переданным url', async () => { + const meta = document.createElement('meta'); + meta.name = 'metrika-id'; + meta.content = '99999999'; + document.head.appendChild(meta); + + const ym = vi.fn(); + // @ts-expect-error стаб глобала + window.ym = ym; + + const { hit } = await import('../../resources/js/plugins/metrika'); + hit('/dashboard'); + expect(ym).toHaveBeenCalledWith(99999999, 'hit', '/dashboard'); + }); +}); diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index ad85b7c2..6bca48a6 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-07-08T01:06:48.909Z +Last updated: 2026-07-08T01:33:18.450Z | Контролёр | Состояние | Детали | |---|---|---| @@ -112,9 +112,9 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 2132 | Code | 9.38ч | NaNч | -| 3276 | MsMpEng | 9.04ч | NaNч | -| 4 | System | 3.76ч | 0.0ч | +| 2132 | Code | 9.47ч | NaNч | +| 3276 | MsMpEng | 9.15ч | NaNч | +| 4 | System | 3.77ч | NaNч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.