From 05b2dff9798e9461daa8c267946ebeb1aa92ea2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Wed, 8 Jul 2026 04:35:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(metrika):=20=D0=BB=D0=B5=D0=BD=D0=B8=D0=B2?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D1=87=D0=B8?= =?UTF-8?q?=D0=BA=20+=20hit=20=D1=81=20=D0=B8=D0=B4=D0=B5=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D1=82=D0=B5=D0=BD=D1=82=D0=BD=D0=BE=D1=81=D1=82=D1=8C?= =?UTF-8?q?=D1=8E=20(TDD,=203=20=D1=82=D0=B5=D1=81=D1=82=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit metrika.ts: читает id из , один раз инжектит tag.js + init webvisor:true (defer), hit(url) шлёт заход. Тесты tests/Frontend/metrika.spec.ts: no-op без meta, идемпотентная загрузка, hit вызывает ym с id+url. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/resources/js/plugins/metrika.ts | 59 +++++++++++++++++++++++++++++ app/tests/Frontend/metrika.spec.ts | 49 ++++++++++++++++++++++++ docs/observer/STATUS.md | 8 ++-- 3 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 app/resources/js/plugins/metrika.ts create mode 100644 app/tests/Frontend/metrika.spec.ts 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-сессий.