You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bb309a8
Browse filesBrowse the repository at this point in the historyBrowse files
fix(telemetria): heartbeat por sesión y watcher de build para sesiones SPA stale
Diagnóstico de pilot_events con 0 filas pese a un admin activo:
el servidor siempre estuvo sano (probe autenticado en prod devolvió
201 e insertó fila), pero el bundle instrumentado sólo llega a pestañas
recargadas; las sesiones SPA abiertas antes del deploy seguían corriendo
código sin telemetría sin mecanismo de actualización.
- beacon: un instrumentation_heartbeat por sesión montada, siempre
incluso en /admin, para detectar un pipeline muerto en minutos
- endpoint: ALLOWED_EVENTS acepta instrumentation_heartbeat
- migración 057: relaja el check de pilot_events.event (aplicada en prod)
- insights: adoption y últimos eventos filtran route_visited para no
diluirse con heartbeats; contratos SQL regenerados en sincronía
- useBuildWatcher: recarga la pestaña cuando el HTML servido referencia
un main asset más nuevo (poll 5 min, guard anti-loop, jitter)
- contrato pilot-telemetry-smoke: aserciones de heartbeat y 057
- tests: beacon batch con heartbeat único + test del watcher
Copy file name to clipboardExpand all lines: db/README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,6 +83,7 @@ Desde `041_schema_migration_baseline.sql`, Neon conserva un registro explícito
83
83
|`054_design_assets.sql`| registry de assets de diseño con lifecycle, provenance y binarios en Neon Object Storage |
84
84
|`055_ml_human_feedback.sql`| feedback humano `good/bad` para Seafood AI/ML con comentario, contexto, revisión posterior y prohibición explícita de auto-retraining |
85
85
|`056_pilot_events.sql`| telemetría de fricción de pilotaje: beacon de rutas visitadas autenticado por sesión, sin query strings ni PII, rol siempre server-side, sólo lectura para el criterio de reactivación UX |
86
+
|`057_pilot_heartbeat.sql`| relaja el check de `pilot_events.event` para admitir `instrumentation_heartbeat` (uno por sesión montada) como red de detección de pipeline muerto, sin estado operacional ni PII |
86
87
87
88
El inventario anterior describe el repositorio actual. Si se agrega una migración, debe agregarse también a esta tabla; CI verifica esa correspondencia y que los landmarks del preflight sigan alineados con el manifiesto runtime.
Copy file name to clipboardExpand all lines: scripts/pilot-insights-queries.mjs
+2-2Lines changed: 2 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -12,8 +12,8 @@ export const META={
12
12
13
13
exportconstQUERIES=[
14
14
{name:"top_routes_7d_30d",template:"select path,\r\n count(*) filter (where created_at>=now()-interval '7 days')::int visits_7d,\r\n count(distinct operator_id) filter (where created_at>=now()-interval '7 days')::int operators_7d,\r\n count(*)::int visits_30d,\r\n count(distinct operator_id)::int operators_30d\r\n from pilot_events\r\n where event='route_visited' and created_at>=now()-interval '30 days'\r\n group by path\r\n order by visits_30d desc,path asc\r\n limit 12",sql:"select path,\r\n count(*) filter (where created_at>=now()-interval '7 days')::int visits_7d,\r\n count(distinct operator_id) filter (where created_at>=now()-interval '7 days')::int operators_7d,\r\n count(*)::int visits_30d,\r\n count(distinct operator_id)::int operators_30d\r\n from pilot_events\r\n where event='route_visited' and created_at>=now()-interval '30 days'\r\n group by path\r\n order by visits_30d desc,path asc\r\n limit 12"},
15
-
{name:"daily_active_operators_14d",template:"select to_char(created_at::date,'YYYY-MM-DD') \"day\",count(distinct operator_id)::int operators\r\n from pilot_events\r\n where created_at>=now()-interval '14 days'\r\n group by created_at::date\r\n order by created_at::date asc",sql:"select to_char(created_at::date,'YYYY-MM-DD') \"day\",count(distinct operator_id)::int operators\r\n from pilot_events\r\n where created_at>=now()-interval '14 days'\r\n group by created_at::date\r\n order by created_at::date asc"},
16
-
{name:"latest_events_50",template:"select e.created_at,e.operator_id,o.full_name operator_name,e.role,e.event,e.path\r\n from pilot_events e left join operators o on o.id=e.operator_id\r\n order by e.id desc\r\n limit 50",sql:"select e.created_at,e.operator_id,o.full_name operator_name,e.role,e.event,e.path\r\n from pilot_events e left join operators o on o.id=e.operator_id\r\n order by e.id desc\r\n limit 50"},
15
+
{name:"daily_active_operators_14d",template:"select to_char(created_at::date,'YYYY-MM-DD') \"day\",count(distinct operator_id)::int operators\r\n from pilot_events\r\n where event='route_visited' and created_at>=now()-interval '14 days'\r\n group by created_at::date\r\n order by created_at::date asc",sql:"select to_char(created_at::date,'YYYY-MM-DD') \"day\",count(distinct operator_id)::int operators\r\n from pilot_events\r\n where event='route_visited' and created_at>=now()-interval '14 days'\r\n group by created_at::date\r\n order by created_at::date asc"},
16
+
{name:"latest_events_50",template:"select e.created_at,e.operator_id,o.full_name operator_name,e.role,e.event,e.path\r\n from pilot_events e left join operators o on o.id=e.operator_id\r\n where e.event='route_visited'\r\n order by e.id desc\r\n limit 50",sql:"select e.created_at,e.operator_id,o.full_name operator_name,e.role,e.event,e.path\r\n from pilot_events e left join operators o on o.id=e.operator_id\r\n where e.event='route_visited'\r\n order by e.id desc\r\n limit 50"},
{name:"ml_feedback_recent_downs",template:"select f.created_at,o.full_name operator_name,left(f.comment,300) comment,left(f.question,220) question,left(f.answer,400) answer\r\n from ml_feedback f left join operators o on o.id=f.operator_id\r\n where f.rating='bad'\r\n order by f.id desc\r\n limit 10",sql:"select f.created_at,o.full_name operator_name,left(f.comment,300) comment,left(f.question,220) question,left(f.answer,400) answer\r\n from ml_feedback f left join operators o on o.id=f.operator_id\r\n where f.rating='bad'\r\n order by f.id desc\r\n limit 10"}
assert(events.includes("operator.role")&&!/body\.role|body\?\.role/.test(events),'pilot events role must come from the session, never the client')
21
22
assert(events.includes("path.includes('?')")&&events.includes("path.includes('#')"),'pilot events must reject query strings and fragments in paths')
22
23
assert(events.includes("'route_visited'"),'pilot events must whitelist the event kind')
24
+
assert(events.includes("'instrumentation_heartbeat'"),'pilot events must accept the instrumentation heartbeat')
23
25
assert(!/email|fullName|full_name|searchParams/.test(events),'pilot events must not read PII or query strings')
24
26
25
27
// Insights endpoint: admin-only read aggregate with zero writes.
@@ -34,6 +36,7 @@ assert(beacon.includes('useLocation')&&beacon.includes('pathname'),'beacon must
34
36
assert(beacon.includes("typeof navigator.sendBeacon!=='function'"),'beacon must no-op when sendBeacon is unavailable')
35
37
assert(beacon.includes("sendBeacon('/api/pilot-events'")&&beacon.includes('keepalive:true'),'beacon must use sendBeacon with a fetch-keepalive fallback')
36
38
assert(beacon.includes("startsWith('/admin')"),'beacon must not instrument admin surfaces')
39
+
assert(beacon.includes('instrumentation_heartbeat'),'beacon must emit one instrumentation heartbeat per session')
37
40
assert(beacon.includes('pagehide'),'beacon must flush on pagehide so navigation is not lost')
38
41
assert(!/URLSearchParams|location\.search|window\.location\.search/.test(beacon),'beacon must never read query strings')
39
42
assert(app.includes('usePilotTelemetry(Boolean(operator))'),'beacon must be wired at the authenticated app level')
assert(app.includes('path="/admin/pilotaje"')&&app.includes('gate("/admin/pilotaje",<AdminPilotaje/>)'),'App.tsx must register the gated pilotaje route')
45
48
assert(migration.includes('create table if not exists pilot_events'),'migration 056 must create pilot_events')
46
49
assert(migration.includes("insert into schema_migrations")&&migration.includes("'056_pilot_events.sql'")&&migration.includes("'applied'"),'migration 056 must record the canonical applied attestation')
50
+
assert(migration057.includes('alter table pilot_events')&&migration057.includes("'instrumentation_heartbeat'"),'migration 057 must relax the pilot_events event check for the heartbeat')
51
+
assert(migration057.includes("insert into schema_migrations")&&migration057.includes("'057_pilot_heartbeat.sql'")&&migration057.includes("'applied'"),'migration 057 must record the canonical applied attestation')
0 commit comments