feat(metrika): ленивый загрузчик + hit с идемпотентностью (TDD, 3 теста)
metrika.ts: читает id из <meta name=metrika-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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Ленивый загрузчик Яндекс.Метрики для портала.
|
||||
* Грузится ТОЛЬКО из роутера при входе в кабинет (meta.layout==='app') — см. router/index.ts.
|
||||
* id счётчика читается из <meta name="metrika-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);
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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-сессий.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user