PostHog — arquitectura y setup
Analítica de producto para idi-front (Angular 19). Toda la integración con
posthog-js está encapsulada en un único servicio (PostHogService); ningún otro
archivo importa posthog-js directamente.
Cómo funciona
Los eventos no van directo a PostHog: pasan por un Cloudflare Worker que actúa de reverse proxy. Así el tráfico se ve first-party y los ad blockers interfieren menos (ver ad blockers).
- Región de la cuenta: US (
us.i.posthog.com). - Todos los clientes/environments mandan al mismo proyecto de PostHog y al mismo
worker. Se distinguen por la propiedad
deployment(ver eventos y paneles).
El worker se despliega en events.4quinas.com.ar . No reutilizar el worker
de otros productos (p. ej. uno bajo events.oxigenoytecnologia.com, de otra empresa):
crea una dependencia sobre infraestructura ajena.
El PostHogService
src/app/services/posthog.service.ts — providedIn: 'root', se autoinicializa en el
constructor. Usa el patrón Strategy:
PostHogStrategy— llamadas reales aposthog-js, cada una envuelta enngZone.runOutsideAngular()para no disparar change detection.NullStrategy— no-ops. Se usa en dev (sin el toggle), así no hay tráfico de red.
Superficie pública (lo único que se usa desde el resto de la app):
capture(event, properties?)
identify(userId, properties)
reset()
isFeatureEnabled(flag) // boolean
getFeatureFlag(flag) // boolean | string | undefined
onFeatureFlagsLoaded(callback)
Qué hace automáticamente
| Evento | Cuándo |
|---|---|
$pageview / $pageleave | En cada navegación SPA (posthog escucha la History API vía defaults: '2025-05-24') |
$identify | Reactivo: effect/subscripción a authState de Firebase. Al haber usuario → identify(uid, { email, name, deployment }) |
reset | Cuando authState pasa a null (logout) |
| Excepciones | capture_exceptions: true |
| Heatmaps + Session recording | Habilitados en el init (passwords siempre enmascarados) |
La identificación es reactiva (no imperativa): se dispara solo con el estado de auth de Firebase, así que cubre login fresco y recarga de página con sesión activa.
El servicio inyecta el Auth de Firebase directamente (no AuthService) para evitar
una dependencia circular: AuthService ya depende de PostHogService para emitir
eventos de negocio.
Configuración por environment
Cada environment de idi-front es un cliente distinto (VPS propio) sobre el mismo
proyecto de PostHog. Los campos viven en src/environments/environment*.ts:
isDevelopment: false, // true SOLO en environment.development.ts
posthogKey: 'phc_...', // Project API Key (misma para todos)
posthogHost: 'https://<dominio-del-worker>', // reverse proxy propio
posthogDeployment: 'gproyectar', // identificador de cliente (distinto por archivo)
posthogDeployment por archivo: gproyectar (base/prod), development, cedit,
demo, pre, csalud, daxsalud, emergencias.
La posthogKey es la Project API Key de PostHog: es pública por diseño (viaja en
el bundle del navegador). No es un secreto y puede commitearse.
Init (resumen)
posthog.init(environment.posthogKey, {
api_host: environment.posthogHost,
ui_host: 'https://us.posthog.com',
defaults: '2025-05-24', // $pageview/$pageleave automáticos en SPA
capture_exceptions: true,
capture_heatmaps: true,
session_recording: { maskAllInputs: false, maskInputOptions: { password: true } },
});
posthog.register({ deployment: environment.posthogDeployment }); // super property
posthog.group('instance', environment.posthogDeployment); // group type
Probar en local
En dev PostHog está apagado por defecto (isDevelopment: true → NullStrategy).
Para activarlo en tu máquina:
- En
environment.development.ts, poné unaposthogKeyreal yposthogHost: 'https://us.i.posthog.com'(directo, sin worker — el proxy solo hace falta en los deploys reales). - En la consola del navegador:
localStorage.setItem('oxi-posthog-dev', '1'); // + recargar// para apagar: localStorage.removeItem('oxi-posthog-dev')
npm start, logueá y navegá. En PostHog → Activity / Live deberías ver$pageview,$identifyy los eventos condeployment: development.
environment.development.ts está gitigneado. Cada dev debe agregar los campos de
PostHog en su copia local.
Instalación (referencia)
cd idi-front
pnpm add posthog-js
La integración inicial se hizo con npx @posthog/wizard, pero el resultado se
refactorizó al patrón de arriba (Strategy + identify reactivo + segmentación por
deployment) y se corrigieron los defectos que dejó el wizard (config por env vars que
no aplica a este builder, y solo 2 de 8 environments configurados).
Código del reverse proxy (Cloudflare Worker)
Es agnóstico de la app: solo reenvía a PostHog. Lo único que cambia entre productos es el dominio donde se despliega.
const API_HOST = "us.i.posthog.com";
const ASSET_HOST = "us-assets.i.posthog.com";
async function handleRequest(request, ctx) {
const url = new URL(request.url);
const pathname = url.pathname;
const pathWithParams = pathname + url.search;
if (pathname.startsWith("/static/") || pathname.startsWith("/array/")) {
return addCorsHeaders(await retrieveAsset(request, pathWithParams, ctx));
}
return addCorsHeaders(await forwardRequest(request, pathWithParams));
}
async function retrieveAsset(request, pathname, ctx) {
let response = await caches.default.match(request);
if (!response) {
response = await fetch(`https://${ASSET_HOST}${pathname}`);
ctx.waitUntil(caches.default.put(request, response.clone()));
}
return response;
}
async function forwardRequest(request, pathWithSearch) {
const ip = request.headers.get("CF-Connecting-IP") || "";
const originHeaders = new Headers(request.headers);
originHeaders.delete("cookie");
originHeaders.set("X-Forwarded-For", ip);
const originRequest = new Request(`https://${API_HOST}${pathWithSearch}`, {
method: request.method,
headers: originHeaders,
body: request.method !== "GET" && request.method !== "HEAD" ? await request.arrayBuffer() : null,
redirect: request.redirect,
});
return await fetch(originRequest);
}
function addCorsHeaders(response) {
const newHeaders = new Headers(response.headers);
newHeaders.set("Access-Control-Allow-Origin", "*");
newHeaders.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
newHeaders.set("Access-Control-Allow-Headers", "*");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
export default {
async fetch(request, env, ctx) {
return handleRequest(request, ctx);
},
};