Analytics
packages/analytics/README.md
@repo/analytics
Paquete de mediciones, analytics y tracking de conversiones para el monorepo Nidus. Centraliza la integración de Google Analytics 4 (GA4), Google Tag Manager (GTM) y Meta (Facebook) Pixel en un único paquete reutilizable.
Cada app configura sus propios IDs a través de variables de entorno y monta los componentes una vez en su layout raíz.
Índice
- Qué resuelve
- Arquitectura del paquete
- Instalación
- Variables de entorno por app
- Uso en el layout raíz
- Tracking de eventos
- API completa
- Tests
- Agregar variables a App Hosting
- Extensión: agregar una nueva plataforma
1. Qué resuelve
| Problema | Solución |
|---|---|
| Código de tracking duplicado en cada app | Un único paquete compartido |
| Scripts lentos que bloquean el FCP | next/script con strategy="afterInteractive" |
Tipos incorrectos para window.fbq / window.dataLayer | Tipos declarados en globals.d.ts |
Re-envío de PageView en navegación SPA | AnalyticsPageViewTracker escucha cambios de ruta |
| IDs de analytics hardcodeados | Variables de entorno por app, pasadas como props |
| XSS al inyectar IDs en scripts inline | Sanitización y validación de formato antes de la inyección |
2. Arquitectura del paquete
packages/analytics/
├── src/
│ ├── index.ts # Tipos + funciones de eventos (sin componentes)
│ ├── types.ts # Todas las interfaces y tipos
│ ├── globals.d.ts # Augmentación de Window (fbq, dataLayer)
│ ├── gtm/
│ │ ├── GTMScript.tsx # RSC — inyecta <script> GTM en <head>
│ │ ├── GTMNoScript.tsx # RSC — <noscript> fallback en <body>
│ │ ├── events.ts # pushGTMEvent, trackGTMPageView
│ │ └── index.ts # Re-exports de @repo/analytics/gtm
│ ├── facebook/
│ │ ├── FacebookPixelScript.tsx # RSC — inyecta pixel init en <body>
│ │ ├── events.ts # trackFbEvent, trackFbCustomEvent, trackFbPageView
│ │ └── index.ts # Re-exports de @repo/analytics/facebook
│ ├── ga4/
│ │ ├── events.ts # trackGA4Event, trackGA4PageView
│ │ └── index.ts # Re-exports de @repo/analytics/ga4
│ ├── hooks/
│ │ └── useAnalyticsTrackEvents.ts # Hook: scroll depth, clicks, section views (GA4)
│ └── tracker/
│ ├── AnalyticsPageViewTracker.tsx # Client Component — re-tracking en cambio de ruta
│ └── index.ts # Re-exports de @repo/analytics/tracker
├── tests/
│ ├── gtm.events.test.ts
│ └── facebook.events.test.ts
└── vitest.config.ts
Entry points del paquete
| Import | Contenido |
|---|---|
@repo/analytics | Tipos + funciones de eventos (GTM + FB + GA4) |
@repo/analytics/ga4 | trackGA4Event, trackGA4PageView |
@repo/analytics/gtm | GTMScript, GTMNoScript, funciones GTM |
@repo/analytics/facebook | FacebookPixelScript, funciones FB Pixel |
@repo/analytics/tracker | AnalyticsPageViewTracker (Client Component) |
Por qué separar los entry points? Los Server Components (
GTMScript,GTMNoScript,FacebookPixelScript) no pueden mezclarse con el'use client'del tracker en algunos bundlers. La separación permite importarlos de forma independiente sin errores.
3. Instalación
En el package.json de cada app que quiera usar analytics:
{
"dependencies": {
"@repo/analytics": "workspace:*"
}
}
pnpm install
4. Variables de entorno por app
Cada app define sus propios IDs. Los componentes reciben el ID como prop desde
process.env.NEXT_PUBLIC_* — nunca se hardcodean IDs dentro del paquete.
| Variable | Descripción | Ejemplo |
|---|---|---|
NEXT_PUBLIC_GTM_ID | Google Tag Manager container ID | GTM-XXXXXXXX |
NEXT_PUBLIC_FB_PIXEL_ID | Meta Pixel numeric ID | 1234567890123456 |
.env.local (desarrollo)
# apps/landing/.env.local
NEXT_PUBLIC_GTM_ID=GTM-XXXXXXXX
NEXT_PUBLIC_FB_PIXEL_ID=1234567890123456
5. Uso en el layout raíz
Montar los scripts una sola vez en app/layout.tsx de cada app.
Los componentes de script son Server Components — no añaden JS al bundle del cliente.
// apps/landing/app/layout.tsx
import { GTMScript, GTMNoScript } from '@repo/analytics/gtm';
import { FacebookPixelScript } from '@repo/analytics/facebook';
import { AnalyticsPageViewTracker } from '@repo/analytics/tracker';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<head>
{/* 1. GTM script — se carga después del FCP */}
<GTMScript gtmId={process.env.NEXT_PUBLIC_GTM_ID} />
</head>
<body>
{/* 2. GTM noscript fallback — justo después de <body> */}
<GTMNoScript gtmId={process.env.NEXT_PUBLIC_GTM_ID} />
{/* 3. Meta Pixel init */}
<FacebookPixelScript pixelId={process.env.NEXT_PUBLIC_FB_PIXEL_ID} />
{/* 4. Re-dispara PageView en cada cambio de ruta (SPA navigation) */}
<AnalyticsPageViewTracker />
{children}
</body>
</html>
);
}
Si una variable de entorno no está definida, el componente simplemente no renderiza nada — no hay errores en runtime.
Deshabilitar plataformas selectivamente
{
/* Solo GTM, sin Facebook Pixel */
}
<AnalyticsPageViewTracker enableFacebook={false} />;
{
/* Solo Facebook Pixel */
}
<AnalyticsPageViewTracker enableGTM={false} />;
6. Tracking de eventos
Importar las funciones desde @repo/analytics en cualquier Client Component.
'use client';
import { pushGTMEvent, trackFbEvent, trackFbCustomEvent, trackGA4Event } from '@repo/analytics';
// ── GA4 ──────────────────────────────────────────────────────────────────────
// Evento personalizado
trackGA4Event('landing_cta_click', {
cta_source: 'hero',
scroll_target: 'waitlist-form',
page_path: window.location.pathname,
});
// Vista de página (manual)
import { trackGA4PageView } from '@repo/analytics/ga4';
trackGA4PageView('/onboarding/paso-2', 'Onboarding — Paso 2');
// ── Hook de tracking automático (scroll depth, clicks, section views) ─────────
import { useAnalyticsTrackEvents } from '@repo/analytics';
// Montar en cualquier Client Component o layout
useAnalyticsTrackEvents({
enableScrollDepth: true, // Emite scroll_depth a GA4 en 25/50/75/90%
enableClickTracking: true, // Registra clicks en elementos con data-track
enableSectionViews: true, // Registra visibilidad de secciones con IntersectionObserver
scrollThresholds: [25, 50, 75, 90],
});
// ── GTM ──────────────────────────────────────────────────────────────────────
// Evento personalizado (cualquier clave/valor)
pushGTMEvent({ event: 'cta_click', button_text: 'Empezar gratis', section: 'hero' });
// Vista de página virtual (útil para modales o steps de un wizard)
import { trackGTMPageView } from '@repo/analytics';
trackGTMPageView('/onboarding/step-2', 'Onboarding — Paso 2');
// ── Facebook Pixel ────────────────────────────────────────────────────────────
// Evento estándar
trackFbEvent('Lead', { content_name: 'formulario_contacto' });
trackFbEvent('Purchase', { value: 4999, currency: 'ARS', num_items: 1 });
trackFbEvent('CompleteRegistration', { status: 'confirmed' });
trackFbEvent('ViewContent', { content_type: 'product', content_ids: ['plan-pro'] });
// Evento personalizado
trackFbCustomEvent('presupuesto_solicitado', { tipo_obra: 'reforma_baño', zona: 'CABA' });
trackFbCustomEvent('video_reproducido', { titulo: 'Demo Nidus', duracion_seg: 42 });
Eventos estándar de Meta Pixel disponibles
PageView · ViewContent · Search · AddToCart · AddToWishlist ·
InitiateCheckout · AddPaymentInfo · Purchase · Lead ·
CompleteRegistration · Contact · CustomizeProduct · Donate ·
FindLocation · Schedule · StartTrial · SubmitApplication · Subscribe
7. API completa
@repo/analytics/ga4
| Export | Tipo | Descripción |
|---|---|---|
trackGA4Event(name, params?) | Función | Emite un evento a GA4 vía gtag('event', ...) |
trackGA4PageView(path, title?) | Función | Emite el evento estándar page_view a GA4 |
@repo/analytics — Hook
| Export | Tipo | Descripción |
|---|---|---|
useAnalyticsTrackEvents(opts) | Hook | Scroll depth, click tracking y section views automáticos enviados a GA4 |
Opciones de useAnalyticsTrackEvents:
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
enableScrollDepth | boolean | true | Activa tracking por profundidad de scroll |
enableClickTracking | boolean | true | Activa tracking de clicks |
enableSectionViews | boolean | true | Activa tracking de visibilidad de sección |
scrollThresholds | Array<number> | [25, 50, 75, 90] | Porcentajes donde se emite el evento |
@repo/analytics/gtm
| Export | Tipo | Descripción |
|---|---|---|
GTMScript | Server Component | Inyecta el snippet GTM en <head> |
GTMNoScript | Server Component | <noscript> fallback en <body> |
pushGTMEvent(data) | Función | Empuja un evento al window.dataLayer |
trackGTMPageView(path, title?) | Función | Empuja un evento page_view a GTM |
@repo/analytics/facebook
| Export | Tipo | Descripción |
|---|---|---|
FacebookPixelScript | Server Component | Inyecta el snippet del pixel y dispara el PageView inicial |
trackFbEvent(event, params?) | Función | Dispara un evento estándar de Meta Pixel |
trackFbCustomEvent(event, params?) | Función | Dispara un evento personalizado de Meta Pixel |
trackFbPageView() | Función | Dispara fbq('track', 'PageView') manualmente |
@repo/analytics/tracker
| Export | Tipo | Descripción |
|---|---|---|
AnalyticsPageViewTracker | Client Component | Dispara PageView en GTM y/o FB Pixel en cada cambio de ruta |
Props de AnalyticsPageViewTracker:
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
enableGTM | boolean | true | Activa tracking de page_view en GTM |
enableFacebook | boolean | true | Activa tracking de PageView en Meta Pixel |
8. Tests
El paquete usa Vitest con entorno jsdom para simular el DOM del navegador.
# Ejecutar tests en watch mode
pnpm test --filter @repo/analytics
# Ejecutar tests una sola vez (CI)
pnpm test:run --filter @repo/analytics
Qué cubren los tests
| Archivo | Qué se prueba |
|---|---|
tests/gtm.events.test.ts | pushGTMEvent, trackGTMPageView, inicialización de dataLayer, SSR |
tests/facebook.events.test.ts | trackFbEvent, trackFbCustomEvent, trackFbPageView, ausencia de fbq, SSR |
Agregar un test nuevo
// tests/mi-nuevo.test.ts
import { describe, it, expect } from 'vitest';
import { pushGTMEvent } from '../src/gtm/events';
describe('Mi test', () => {
it('...', () => {
pushGTMEvent({ event: 'mi_evento' });
expect(window.dataLayer[0]?.event).toBe('mi_evento');
});
});
9. Agregar variables a App Hosting
Firebase App Hosting lee la configuración desde el archivo apphosting.yaml
(producción) o apphosting.test.yaml (ambiente de prueba) de cada app.
Para la app landing
# apps/landing/apphosting.yaml (o apphosting.test.yaml)
env:
# ── Analytics ──────────────────────────────────────────────────────────────
# Google Tag Manager — container ID (formato GTM-XXXXXXXX)
- variable: NEXT_PUBLIC_GTM_ID
value: 'GTM-XXXXXXXX'
availability:
- BUILD
- RUNTIME
# Meta (Facebook) Pixel — ID numérico de 15-16 dígitos
- variable: NEXT_PUBLIC_FB_PIXEL_ID
value: '1234567890123456'
availability:
- BUILD
- RUNTIME
availability: [BUILD, RUNTIME]es necesario porque Next.js embebe las variablesNEXT_PUBLIC_*en el bundle durante el build. SinBUILD, el valor seríaundefineden el cliente.
Para otras apps (01-base-app, etc.)
Repetir el mismo bloque en el apphosting.yaml correspondiente con los IDs
propios de cada app. Cada app tiene su propio GTM container y Pixel ID — no compartir IDs entre apps.
Variables secretas (si el ID es sensible)
Si el pixel ID no debe ser visible en el repositorio, usar el Secret Manager de Firebase:
- variable: NEXT_PUBLIC_FB_PIXEL_ID
secret: fb-pixel-id-landing
availability:
- BUILD
- RUNTIME
10. Extensión: agregar una nueva plataforma
Ejemplo: agregar TikTok Pixel.
- Crear
src/tiktok/TikTokPixelScript.tsx(Server Component con validación de ID). - Crear
src/tiktok/events.tscon las funciones de tracking (ttq.track, etc.). - Crear
src/tiktok/index.tscon los re-exports. - Agregar el nuevo entry point en
package.json:json"./tiktok": { "types": "./src/tiktok/index.ts", "import": "./src/tiktok/index.ts", "default": "./src/tiktok/index.ts" } - Montar el componente en el layout y agregar
NEXT_PUBLIC_TIKTOK_PIXEL_IDenapphosting.yaml. - Agregar tests en
tests/tiktok.events.test.ts.
Contribución y validación
pnpm lint --filter @repo/analytics
pnpm check-types --filter @repo/analytics
pnpm test:run --filter @repo/analytics
pnpm lint --filter @repo/01-base-package pnpm build --filter @repo/01-base-package pnpm check-types --filter @repo/01-base-package
## 6. Buenas practicas
- API chica y coherente.
- Tipos explicitos para todos los contratos.
- README actualizado por cada cambio de API.