# SDK web

## Para qué sirve

El SDK web mide la experiencia real de quienes usan tu sitio: cuánto tarda en cargar cada pantalla, qué errores ven, qué pedidos al servidor fallan o tardan y qué hacen (eventos de negocio). Manda los datos a tu punto de entrada de Lymen y une cada pedido del navegador con la traza de tu servidor.

- Pesa unos **34 KB comprimido** (el bundle para pegar con `<script>`).
- **No graba sesiones** ni lee lo que la gente escribe en los formularios.
- Aplica las reglas de **Privacidad** del portal antes de mandar nada.

## Cuándo usarlo

Cuando tenés un sitio o una aplicación web (React, Angular, Vue o simple HTML) y querés medir lo que vive cada visitante. Antes tenés que dar de alta el sitio en [Aplicaciones](https://lymen.app/docs/instalacion/aplicaciones/): ahí obtenés tu **clave** (`ly_pub_…`) y el código listo para copiar.

## Instalación

### Con un script (cualquier sitio)

Pegá esto en el `<head>` de todas las páginas, con tu clave:

```html
<script src="https://<tu-cuenta>-ingesta.lymen.app/lymen-sdk-web.js"
        data-sitio="ly_pub_XXXXXXXXXXXXXXXXXXXXXXXX"
        data-version="1.4.0"
        async></script>
```

El script se inicia solo con los atributos `data-*` y deja la API en `window.lymen`. Si necesitás llamar a la API antes de que termine de cargar, encolá las llamadas:

```html
<script>
  window.lymenCola = window.lymenCola || [];
  lymenCola.push(['usuario', 'paciente-123']);
</script>
```

### Con npm

```sh
npm install lymen-sdk-web
```

```ts
import { iniciar } from 'lymen-sdk-web';

iniciar({
  sitio: 'ly_pub_XXXXXXXXXXXXXXXXXXXXXXXX',
  endpoint: 'https://<tu-cuenta>-ingesta.lymen.app',
  version: '1.4.0',
  propagarA: ['https://api.tu-empresa.com'],
});
```

Trae ESM, CommonJS y tipos de TypeScript. Llamarlo del lado del servidor (SSR) no hace nada.

## Opciones

La tabla muestra el nombre en `iniciar()` y, cuando existe, el atributo del script.

| Opción (atributo) | Para qué sirve | Por defecto | Valores válidos | Impacto de cambiarlo |
|---|---|---|---|---|
| `sitio` (`data-sitio`) | Clave pública del sitio. | — (obligatoria) | `ly_pub_…` | Sin ella, el SDK no arranca y avisa en la consola. No es un secreto. |
| `version` (`data-version`) | Versión de tu aplicación. | — (obligatoria en npm) | Texto | Permite comparar errores y rendimiento entre versiones de tu sitio. |
| `endpoint` (`data-endpoint`) | Punto de entrada de Lymen. | Con el script: el origen de donde se bajó. Con npm: indicalo siempre. | URL `https://<tu-cuenta>-ingesta.lymen.app` | Si es incorrecto, no llega nada. |
| `servicio` (`data-servicio`) | Nombre del servicio. | La clave del sitio | Texto | El servidor lo reemplaza por el nombre de servicio del sitio que cargaste en el portal. |
| `entorno` (`data-entorno`) | Entorno. | `produccion` | Texto | Permite separar producción de pruebas en los filtros. |
| `propagarA` (`data-propagar-a`) | Orígenes de tus APIs a los que se les agrega `traceparent`, la marca que une el pedido del navegador con la traza del servidor. | Vacío | Lista (en el atributo, separada por comas). Acepta comodín: `https://*.tu-empresa.com`. | Solo a esos orígenes (ni siquiera al mismo origen si no está). Tu API tiene que aceptar `traceparent` en CORS, o el navegador bloquea el pedido. Se suma a lo configurado en el portal. |
| `muestreo` (`data-muestreo`) | Qué parte de las sesiones se mide. | `1` (todas) | De 0 a 1 | **Costo.** Menos sesiones, menos datos. **Manda el valor del portal si existe.** Se decide al empezar cada sesión. |
| `consentimiento` (`data-consentimiento`) | Estado inicial del consentimiento. | Lo último que eligió la persona; si nunca eligió, `concedido` | `pendiente`, `concedido`, `denegado` | **Legal y privacidad.** Con `pendiente` se guarda en memoria y no se manda nada; con `denegado`, no se mide ni se manda nada. Si tu sitio pide consentimiento (cookies), iniciá en `pendiente`. |
| `capturarClics` (`data-capturar-clics`) | Registra los clics. | `false` | `true` / `false` | **Privacidad.** Guarda qué se tocó (el atributo `data-lymen-clic`, el id o un selector corto), nunca el texto. El portal puede mandar sobre este valor. |
| `capturarConsola` (`data-capturar-consola`) | Manda `console.warn` y `console.error` como mensajes. | `false` | `true` / `false` | **Privacidad.** Los mensajes pueden traer datos personales (pasan por Privacidad). Solo llegan si el portal recibe *Mensajes de la app*, que viene apagado. |
| `rutas` (`data-rutas`) | Patrones de las rutas de tu sitio para nombrar las vistas. | — | Por ejemplo `['/turnos/:id', '/medicos/:slug']` | Sin patrones, los ids se reemplazan solos por `:id`. Con patrones tus vistas se agrupan como querés. |
| `normalizarRuta` | Función propia para nombrar las vistas. | — | Función | Solo con npm. |
| `vistasAutomaticas` | Detecta los cambios de pantalla. | `true` | `true` / `false` | En `false`, usá `vista()` a mano. |
| `red` | Mide los pedidos `fetch`/`XMLHttpRequest`. | `true` | `true` / `false` | En `false` no hay datos de pedidos ni `traceparent`. |
| `depurar` | Avisos del SDK en la consola. | `false` | `true` / `false` | Solo para desarrollo. |

El portal tiene la última palabra sobre el muestreo, los clics y a qué orígenes se propaga `traceparent` (se suma). Un cambio en el portal llega solo, sin publicar una versión nueva del sitio.

## API

```ts
usuario(id | null)              // se hashea con SHA-256 antes de salir del navegador
evento('turno.reservado', { monto: 15000, especialidad: 'clinica' })
error(err, { 'turno.paso': 'confirmar' })   // error que ya manejaste
vista('Detalle del turno')      // vista manual
consentimiento('concedido' | 'denegado')
log('warn', 'No se pudo cargar la agenda', { 'turno.id': 9 })   // debug | info | warn | error
```

- **Nombre de los eventos de negocio**: `entidad.accion`, en minúsculas, números y `_` (por ejemplo `turno.reservado`).
- **`log`**: el mensaje se corta a 4096 caracteres y pasa por Privacidad. Solo llega si en el portal está encendido *Mensajes de la app*.

### Eventos de negocio: desde un solo lado

Mandá un mismo evento (por ejemplo `turno.reservado`) **desde la app o desde el backend, nunca desde los dos**: Lymen junta los dos orígenes y se contaría doble. Mandalo desde donde se confirma de verdad (normalmente el backend). Desde el sitio quedan los eventos que solo existen ahí (`carrito.abierto`, `filtro.aplicado`).

## Qué mide

| Dato | Detalle |
|---|---|
| **Sesión** | Id al azar, compartido entre pestañas. Se renueva a los 30 minutos sin actividad o a las 4 horas. |
| **Vistas** | Cada cambio de ruta (history o `#/`), con la vista anterior y cuánto estuvo la persona. Las rutas se normalizan (`/turnos/123` pasa a `/turnos/:id`). |
| **Qué tan rápido carga** | LCP, INP, CLS, FCP y TTFB, calificados como bueno, mejorable o malo, y los tiempos de carga de la página. |
| **Errores** | Los no manejados y las promesas rechazadas, más los que mandes con `error()`. Cada uno trae una huella para agruparlo aunque cambie el mensaje o el archivo en cada versión. |
| **Pedidos al servidor** | Método, URL, estado y duración. |
| **Tareas largas** | Momentos en que la página se trabó. |
| **Clics** | Solo si los activás. |
| **Eventos de negocio** y **mensajes** | Los que mandes con `evento()` y `log()`. |

Los datos se envían en lotes cada 5 segundos o cada 50 elementos, y lo pendiente sale al ocultarse o cerrarse la página.

## Privacidad

- Las reglas vienen de **Privacidad** en el portal. El SDK las pide al iniciar y cada 10 minutos, y guarda la última copia para arrancar sin red.
- Se aplican a URLs, mensajes de error, textos y atributos: enmascaran (`***`), borran o hashean.
- Si el SDK nunca pudo bajar las reglas, usa unas de respaldo: DNI, email, tarjetas (solo si el número es válido) y CUIT/CUIL.
- La plataforma vuelve a aplicar Privacidad al recibir: el navegador es la primera barrera, no la única.
- No se leen los valores de formularios y el id de usuario sale hasheado.

## Ejemplos

### React

```tsx
// main.tsx
import { iniciar, usuario, evento, error } from 'lymen-sdk-web';

iniciar({
  sitio: import.meta.env.VITE_LYMEN_SITIO,
  endpoint: 'https://<tu-cuenta>-ingesta.lymen.app',
  version: import.meta.env.VITE_VERSION,
  propagarA: ['https://api.tu-empresa.com'],
  rutas: ['/turnos/:id'],
});

usuario(sesion.idPaciente);   // después del login

async function confirmar() {
  try {
    await api.reservar(datos);
  } catch (e) {
    error(e, { 'turno.paso': 'confirmar' });
  }
}
```

React Router usa `history`, así que las vistas se detectan solas.

### Angular

```ts
// main.ts
import { iniciar } from 'lymen-sdk-web';

iniciar({
  sitio: environment.lymenSitio,
  endpoint: 'https://<tu-cuenta>-ingesta.lymen.app',
  version: environment.version,
  propagarA: [environment.apiUrl],
  rutas: ['/turnos/:id', '/medicos/:slug'],
});
bootstrapApplication(AppComponent, appConfig);
```

Para los errores que Angular atrapa antes que el navegador, implementá un `ErrorHandler` que llame a `error(e, { origen: 'angular' })`.

## Problemas comunes

| Síntoma | Qué hacer |
|---|---|
| No llega nada | Revisá `sitio` y `endpoint`; en la consola, activá `depurar: true`. Confirmá que el sitio esté en **Recibir datos** y que su dirección esté en la lista de direcciones del alta. |
| Aparece "iniciar necesita sitio y version" | Falta una de las dos opciones obligatorias. |
| El navegador bloquea pedidos a mi API | Tu API tiene que aceptar el encabezado `traceparent` en CORS, o quitá esa API de `propagarA`. |
| Veo eventos de negocio duplicados | Los estás mandando desde el sitio y desde el backend. Elegí un solo lado. |
| No veo mensajes (`log`) | En el portal, el tipo *Mensajes de la app* viene apagado. |
| Las vistas tienen ids en el nombre | Usá la opción `rutas` con los patrones de tu router. |
