Registros de auditoría y eventos de webhook
Lo que el kit registra por su cuenta — cambios en modelos, cada correo y SMS, cada webhook — y dónde leerlo.
Hay tres cosas que se escriben sin que nadie lo pida: qué cambió en un registro y quién lo cambió, cada correo y SMS que la aplicación envió o no pudo enviar, y cada webhook que entregó un proveedor de pagos. Esta guía dice qué captura cada registro, dónde lo lees en el panel de administración, cuánto tiempo se conserva y cómo hacer que tus propios modelos y webhooks participen.
El registro de actividad
Cada modelo que se suma escribe una fila en la tabla activity_log cada vez que se crea, actualiza o elimina, con los atributos que cambiaron y el usuario que tenía la sesión. El registro es laravel-activitylog de Spatie, configurado en config/activitylog.php:
| Clave | Variable de entorno | Valor por omisión | Qué hace |
|---|---|---|---|
enabled |
ACTIVITY_LOGGER_ENABLED |
true |
false detiene toda escritura. Las pantallas se quedan; la tabla deja de crecer. |
delete_records_older_than_days |
— | 365 |
La antigüedad a partir de la cual activitylog:clean elimina. |
activity_model |
— | WeblaborMx\TallUtils\Models\Activity |
El modelo con el que se leen las filas. Agrega búsqueda y un diff por campo. |
table_name |
— | activity_log |
|
database_connection |
ACTIVITY_LOGGER_DB_CONNECTION |
vacío (la conexión por omisión) | Apunta el registro a otra base de datos cuando la principal no deba cargarlo. |
Qué guarda una fila
| Columna | Contenido |
|---|---|
event y description |
created, updated o deleted; también restored en un modelo con SoftDeletes. |
subject_type, subject_id |
El registro que cambió. |
causer_type, causer_id |
El usuario con sesión. Vacío cuando el cambio corrió desde un comando, un job o un webhook. |
properties |
JSON. attributes guarda los valores después del cambio; en una actualización, old guarda los de antes. |
El trait del kit, WeblaborMx\TallUtils\Models\WithActivityLog, fija las opciones: se registra cada atributo no protegido (los modelos del kit usan $guarded = [], así que eso es cada columna), sólo se escriben los atributos que realmente cambiaron, una actualización que no tocó nada no escribe fila, y updated_at nunca forma parte del diff. Los modelos que lo traen de fábrica son User, Role, Permission, Announcement, Category y, cuando la facturación está instalada, BillingPlan, BillingAddon, Subscription y SubscriptionItem.
Agrégalo a un modelo tuyo
use WeblaborMx\TallUtils\Models\WithActivityLog;
class Invoice extends Model
{
use WithActivityLog;
public $dont_log = ['viewed_at', 'notes'];
}
$dont_log es opcional. Los atributos que nombra quedan fuera de properties, y un guardado que cambió sólo esos atributos no escribe fila. No hace falta nada más: el trait registra los eventos del modelo por sí mismo.
Léelo
La pantalla es /admin/activities, bajo Logs en la barra lateral, sólo índice y detalle. El índice muestra evento, sujeto y causante; el detalle agrega la descripción y el JSON de properties. Filtra por tipo de sujeto, id de sujeto, tipo de causante, id de causante o evento. El cuadro de búsqueda coincide con la descripción, el tipo de sujeto, el tipo de causante, el evento, el id y el propio JSON.
Su política es más estricta que la del resto del panel: la lista abre sólo para una cuenta que tiene el rol de administrador y está en la lista sudo de config/app.php, así que la entrada del menú es invisible para cualquier otro administrador. Mira /help/sudo-and-super-administrators. Las mismas filas alimentan la métrica Eventos del registro de actividad del dashboard, descrita en /help/dashboard-metrics-and-visitor-tracking.
Evita que crezca para siempre
php artisan activitylog:clean # elimina filas de más de 365 días
php artisan activitylog:clean --days=90 # o de más días de los que indiques
El comando no está programado: routes/console.php sólo programa las estadísticas diarias. Agrégalo tú cuando la tabla deba podarse sola:
// routes/console.php
Schedule::command('activitylog:clean')->daily();
Corre a través del scheduler, así que el servidor necesita php artisan schedule:run en su cron. Mira /help/queues-and-scheduled-work.
Registros de comunicación
Cada correo y SMS enviado a través de una notificación de Laravel deja una fila en communication_logs, haya salido o haya fallado. Lo hacen dos listeners, y Laravel los descubre por su cuenta, así que no hay nada que registrar:
App\Listeners\LogNotificationSentreacciona aNotificationSenty escribe una filasent.App\Listeners\LogNotificationFailedreacciona aNotificationFailedy escribe una filafailed.
Los dos actúan sólo sobre el canal mail y sobre App\Channels\SmsChannel. Las notificaciones de base de datos y de web push no se registran, y tampoco el correo enviado con el facade Mail fuera de una notificación, porque eso nunca dispara el evento.
| Columna | Contenido |
|---|---|
user_id |
La cuenta del destinatario cuando el notificable es un User; vacío en otro caso. |
type |
email o sms (App\Enums\CommunicationType). |
status |
sent o failed (App\Enums\CommunicationStatus). |
recipient |
La dirección a la que el notificable enruta el correo, o su email; para SMS, su phone. |
notification_type |
La clase de la notificación. |
content |
El asunto, leído del toMail() de la notificación. Sólo correo. |
body |
El cuerpo HTML del mensaje que realmente se envió. Sólo correo. |
created_at |
Cuándo se despachó la notificación. |
Una fila fallida no lleva asunto ni cuerpo. Las dos migraciones que crean la tabla, 2026_03_14_000001_create_communication_logs_table y 2026_03_15_000001_add_body_to_communication_logs_table, vienen con el kit y corren con php artisan migrate.
La pantalla es /admin/communication_logs, bajo Logs, de la más reciente a la más antigua, con destinatario, tipo, estado, clase de notificación y fecha de envío en el índice, y el asunto y el cuerpo en el detalle. Filtra por tipo y por estado. Crear y editar están negados por App\Policies\CommunicationLogPolicy; leer requiere el permiso retrieve communication_log. La página de detalle de cada usuario en /admin/users/{id} también lista los mensajes enviados a ese usuario.
No hay variable de entorno para este registro ni comando que lo pode. Cuando la tabla necesite un límite, escribe la limpieza tú mismo.
Eventos de webhook
Stripe entrega sus eventos en POST /api/stripe/webhook, declarado en routes/api.php con dos middleware, en este orden:
->middleware(['webhook', 'log.webhook:stripe']);
webhook es el VerifyWebhookSignature de Cashier; log.webhook es App\Http\Middleware\LogWebhookEvent, y el parámetro después de los dos puntos es el source con el que se guarda la fila. Los dos alias están declarados en bootstrap/app.php.
La firma se revisa primero, así que una petición que Stripe no firmó se rechaza con un 403 antes de escribir nada: las llamadas falsificadas nunca llegan al registro. El secreto y la tolerancia salen de App\Providers\AppServiceProvider::register(), que fija cashier.webhook.secret desde services.stripe.webhook_secret y cashier.webhook.tolerance en 300 segundos:
| Variable de entorno | Para qué sirve |
|---|---|
STRIPE_WEBHOOK_SECRET |
El secreto de firma del endpoint que creaste en el dashboard de Stripe. Una petición firmada hace más de 300 segundos se rechaza. |
Cuando el paquete de facturación no está instalado, la ruta responde 204 y no hace nada más; la fila se escribe de todos modos.
Qué guarda una fila
| Columna | Contenido |
|---|---|
source |
El parámetro del middleware: stripe. |
event_type |
La clave type del cuerpo JSON, o unknown cuando no hay. |
payload |
El cuerpo completo, como JSON. |
response |
{"status": 200}: el estado HTTP con el que respondió el manejador. Vacío cuando lanzó una excepción. |
status |
success, o failed cuando el estado fue 400 o mayor o el manejador lanzó una excepción (App\Enums\WebhookStatus). |
error |
HTTP 4xx, o el mensaje y la traza de la excepción. |
Una excepción se registra y luego se vuelve a lanzar, así que Stripe sigue recibiendo un error y reintenta con su propio calendario.
Léelo
/admin/webhook-events es una pantalla Livewire (App\Livewire\Admin\WebhookEvents) dentro del grupo de rutas de administración, así que necesita el rol de administrador. No está en la barra lateral: ábrela por URL. Lista 25 eventos por página, del más reciente al más antiguo, con un filtro de texto por origen y otro por tipo de evento, ambos por coincidencia parcial. Haz clic en una fila para abrir el payload, la respuesta y el error formateados en un modal.
Qué vuelve a leer las filas
App\Models\WebhookEvent::scopePaidSubscriptionInvoices() selecciona los eventos invoice.paid exitosos de Stripe cuyo amount_paid es mayor que cero y que pertenecen a una suscripción. El paquete de facturación usa esa selección para la métrica Ingresos diarios del dashboard y para decidir qué fechas y monedas necesitan un tipo de cambio. Así que la tabla de webhooks es el registro de ingresos: un endpoint que estuvo caído un día es un día sin ingresos en la gráfica. Mira /help/currencies-intervals-and-exchange-rates y /help/turn-on-plans-and-billing.
Registra un webhook de otro proveedor
El middleware es genérico. Dale otro nombre de origen en cualquier ruta:
Route::post('/paypal/webhook', PaypalWebhookController::class)
->middleware('log.webhook:paypal');
Lee event_type de una clave type de primer nivel, así que un proveedor que nombra sus eventos de otra forma queda registrado como unknown hasta que adaptes el middleware. No verifica firmas: pon la verificación propia del proveedor delante, como lo hace la ruta de Stripe.