@eiaifactory/monitor 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +60 -22
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -182,7 +182,7 @@ rechazan uno por uno con `lote_excedido`) y **hasta 64 KB** (si no, `413` y se r
182
182
  | `user_agent` | string | no | 512 | |
183
183
  | `contexto` | objeto plano de escalares | no | 2 KB | Claves sensibles → `[redactado]` |
184
184
  | `repeticiones` | entero ≥ 1 | no | — | Colapso local. Default 1 |
185
- | `huella_manual` | string | no | 128 | Reemplaza la agrupación calculada (`cron:offers-expire`) |
185
+ | `huella_manual` | string | no | 128 | Reemplaza la agrupación calculada (`cron:<función>`, como hace `app.run_job`) |
186
186
 
187
187
  Los topes son en caracteres. Un texto más largo se recorta con la marca `…[cortado]`.
188
188
 
@@ -198,8 +198,11 @@ Los topes son en caracteres. Un texto más largo se recorta con la marca `…[co
198
198
  | `405` | — | Método que no es `POST` ni `OPTIONS` |
199
199
 
200
200
  Cómo se agrupa (lo hace finance, no el emisor): mismo origen, tipo, mensaje (sin ids, hashes ni
201
- números) y primer frame del stack (sin el hash del chunk de Vite ni línea y columna) son el mismo
202
- problema. `huella_manual` reemplaza ese cálculo.
201
+ números) y primer frame del stack son el mismo problema. Del frame se sacan el hash del chunk de
202
+ Vite (también los que traen `-` o `_`), la línea y la columna, y en un chunk hasheado también el
203
+ nombre de función, que el minificador cambia en cada build: el mismo error no se parte en un
204
+ problema por deploy. Sin stack, cuentan sólo origen, tipo y mensaje. `huella_manual` reemplaza ese
205
+ cálculo.
203
206
 
204
207
  ### Con `curl`
205
208
 
@@ -213,48 +216,83 @@ curl -X POST "https://qoyskdjcjyjxcnhruhni.supabase.co/functions/v1/monitor-inge
213
216
  ### Desde un cron de la base (`net.http_post`)
214
217
 
215
218
  El patrón es envolver cada job en una función que ataja el error y avisa, con la clave en el vault
216
- del cliente:
219
+ del cliente. Es la migración que corre en producción en Mercado Fratelli
220
+ (`20260929130000_monitor_run_job.sql`). Sólo cambian el ejemplo del cron, la referencia al plan de
221
+ Fratelli en el `comment` y un comentario sobre `PT4xx`. Tres decisiones que no son obvias:
222
+
223
+ - **No re-lanza la excepción.** `net.http_post` encola el pedido en una tabla y un worker lo manda
224
+ después: si el job re-lanzara, la transacción se revertiría y el aviso con ella. Deja el error en
225
+ los logs de Postgres con `raise warning`. Consecuencia: en `cron.job_run_details` el job figura
226
+ `succeeded` aunque haya fallado. La falla se ve en el Monitor.
227
+ - **Sin `monitor_clave` en el vault no manda nada.** La clave se carga a mano por ambiente, nunca en
228
+ la migración, que es la misma en dev y en prod. Así la migración se puede aplicar antes de que
229
+ exista la clave.
230
+ - **Sólo pg_cron la llama**: `revoke` para `public`, `anon` y `authenticated`. Es `security definer`.
217
231
 
218
232
  ```sql
233
+ create extension if not exists pg_net with schema extensions;
234
+
219
235
  create or replace function app.run_job(p_fn text)
220
236
  returns void
221
237
  language plpgsql
222
238
  security definer
239
+ -- Vacío: todo va calificado. Las funciones de los crons fijan su propio `search_path`.
223
240
  set search_path = ''
224
241
  as $$
225
242
  declare
226
243
  v_estado text;
227
244
  v_mensaje text;
245
+ v_clave text;
228
246
  begin
247
+ -- El cast a `regproc` valida que la función exista y la escribe bien citada. Adentro del
248
+ -- bloque a propósito: un nombre mal escrito en el cron también es un cron que falla.
229
249
  execute format('select %s()', p_fn::regproc);
230
250
  exception when others then
231
251
  get stacked diagnostics v_estado = returned_sqlstate, v_mensaje = message_text;
252
+
232
253
  -- PT4xx es un rechazo de negocio, no un incidente.
233
254
  if v_estado not like 'PT4%' then
234
- perform net.http_post(
235
- url := 'https://qoyskdjcjyjxcnhruhni.supabase.co/functions/v1/monitor-ingesta',
236
- headers := jsonb_build_object(
237
- 'Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'monitor_clave'),
238
- 'Content-Type', 'application/json'
239
- ),
240
- body := jsonb_build_object('eventos', jsonb_build_array(jsonb_build_object(
241
- 'id_externo', gen_random_uuid(),
242
- 'origen', 'db',
243
- 'mensaje', v_mensaje,
244
- 'codigo', v_estado,
245
- 'ubicacion', p_fn,
246
- 'huella_manual', 'cron:' || p_fn
247
- )))
248
- );
255
+ select decrypted_secret into v_clave
256
+ from vault.decrypted_secrets
257
+ where name = 'monitor_clave';
258
+
259
+ if v_clave is not null then
260
+ perform net.http_post(
261
+ url := 'https://qoyskdjcjyjxcnhruhni.supabase.co/functions/v1/monitor-ingesta',
262
+ headers := jsonb_build_object(
263
+ 'Authorization', 'Bearer ' || v_clave,
264
+ 'Content-Type', 'application/json'
265
+ ),
266
+ body := jsonb_build_object('eventos', jsonb_build_array(jsonb_build_object(
267
+ 'id_externo', gen_random_uuid(),
268
+ 'origen', 'db',
269
+ 'mensaje', v_mensaje,
270
+ 'codigo', v_estado,
271
+ 'ubicacion', 'cron ' || p_fn,
272
+ -- Un problema por cron, sea cual sea el mensaje de cada falla.
273
+ 'huella_manual', 'cron:' || p_fn
274
+ )))
275
+ );
276
+ end if;
249
277
  end if;
250
- -- Sin re-raise: `net.http_post` encola en una tabla, y si la transacción se revierte el
251
- -- aviso se revierte con ella.
278
+
279
+ raise warning 'app.run_job(%): % (SQLSTATE %)', p_fn, v_mensaje, v_estado;
252
280
  end;
253
281
  $$;
254
282
 
255
- -- select cron.schedule('offers-expire', '*/5 * * * *', $$select app.run_job('app.offers_expire')$$);
283
+ comment on function app.run_job(text) is
284
+ 'Corre la función de un cron y, si falla con algo que no es PT4xx, avisa al Monitor de finance (net.http_post con monitor_clave del vault). No re-lanza: el aviso se revertiría con la transacción.';
285
+
286
+ -- Sólo pg_cron (corre como postgres) la llama: ni un usuario ni la API.
287
+ revoke all on function app.run_job(text) from public, anon, authenticated;
288
+
289
+ -- Cada cron pasa por el wrapper. `cron.schedule` con un nombre que ya existe actualiza ese job.
290
+ select cron.schedule('<nombre-del-job>', '0 3 * * *', $$select app.run_job('app.<funcion_del_cron>');$$);
256
291
  ```
257
292
 
293
+ El esquema `app` tiene que existir y no estar expuesto por la API. La clave va al vault de cada
294
+ ambiente: `select vault.create_secret('<clave>', 'monitor_clave');`.
295
+
258
296
  ## Qué NO hace
259
297
 
260
298
  - **Sin sourcemaps**: el stack llega minificado; se agrupa igual por frame normalizado.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eiaifactory/monitor",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Manda los errores de un sistema de cliente al Monitor de EI AI Factory, sin que el usuario final lo note.",
5
5
  "license": "MIT",
6
6
  "type": "module",