Guía · Laravel 10+ · PHP 8.2+
Analítica sin cookies para Laravel.
Un paquete de Composer y una variable de entorno: tu app empieza a contar páginas vistas desde un middleware. Sin JavaScript, sin cookies y sin que la IP de tus visitantes salga de tu servidor.
Después, Rom::track() para lo que importa, y tu proveedor de pagos para el dinero.
Revisado el 14 de octubre de 2026
Instalación: un paquete y un token
El paquete se registra solo (autodiscovery) y añade su middleware TrackPageViews al grupo web. Con RUNOMINE_TOKEN en tu .env, ya cuenta.
¿Prefieres no tocar nada? runomine init detecta Laravel, crea el sitio, escribe el token en tu .env sin tocar lo demás, instala el paquete y espera a la primera visita.
Beta: el paquete aún no está en Packagist. Hasta entonces se instala desde su repositorio público, runomine/runomine-php.
# Beta: from the public repository until it's on Packagist
$ composer config repositories.runomine vcs https://github.com/runomine/runomine-php
$ composer require runomine/runomine-php:dev-main
# .env: the middleware starts counting pageviews
RUNOMINE_TOKEN=rm_live_…
# Or let the CLI do all of the above
$ runomine init
Qué cuenta el middleware, y qué no
- Cuenta las peticiones
GETque responden200context/html: lo que tu visitante vio de verdad. - Lo hace en
terminate(), cuando la respuesta ya ha salido. Tu visitante no espera. - Se salta
api/*,livewire/*, Horizon, Telescope, Pulse y otras rutas internas, y los prefetch del navegador. Añade tuadmin/*enskip. - Las visitas de Inertia y otras navegaciones en el cliente responden JSON, así que no las ve; para esas, y para las páginas que sirve la caché de tu CDN, añade el beacon.
- Detrás de un balanceador, configura los proxies de confianza: el hash usa
$request->ip(), y sin eso todos tus visitantes parecerían el balanceador.
// config/runomine.php (php artisan vendor:publish --tag=runomine-config)
'skip' => ['api/*', 'up', 'livewire/*', 'horizon*', …, 'admin/*'],
'user' => null, // the signed-in user's id; false: never send it
// bootstrap/app.php (Laravel 11+): behind a load balancer, so $request->ip() is the visitor's
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
# .env, only behind Cloudflare without trusted proxies
RUNOMINE_IP_HEADER=CF-Connecting-IP
Eventos de negocio con Rom::track
Dentro de una petición, Rom::track() añade el hash del visitante, la URL, el país (si tu CDN lo envía) y el id del usuario que ha iniciado sesión.
En jobs, comandos y webhooks no hay petición, así que el evento va sin visitante; nunca uno inventado. Pásale el usuario, y runomine lo une a sus visitas anteriores, o entrega al job el visitante con Rom::visitor().
use Runomine\Laravel\Facades\Rom;
// In a request: visitor hash, URL, country and user id go with it
Rom::track('signup');
Rom::track('checkout_started', ['plan' => 'pro']);
// A queued job has no request: hand it the visitor…
ProcessCheckout::dispatch($order, Rom::visitor());
// …and in the job
Rom::track('checkout_completed', context: ['visitor' => $this->visitor]);
// Or name the user, and runomine links it to their earlier visits
Rom::track('trial_converted', user: $team->owner_id);
Ventas de Stripe o Paddle, por fuente
Los importes no salen de tu código: conecta Stripe, Paddle, Lemon Squeezy, Shopify o PayPal en los ajustes del sitio y runomine lee cada venta, renovación y reembolso.
Para saber qué fuente trajo cada venta, pasa Rom::checkoutMetadata() al crear el checkout. Lleva el visitante de esa petición y tu id de usuario; runomine los lee de vuelta del proveedor. En suscripciones de Stripe, ponlo también en subscription_data para que las renovaciones lleven al usuario.
Con un proveedor conectado, no envíes además revenue con Rom::track('purchase'): la venta contaría dos veces.
// ['runomine_visitor' => '5f9e1bebe31e8bf5', 'runomine_user' => '42']
$metadata = Rom::checkoutMetadata();
$session = $stripe->checkout->sessions->create([
'mode' => 'subscription',
'line_items' => [['price' => 'price_123', 'quantity' => 1]],
'success_url' => route('billing.done'),
'client_reference_id' => (string) $user->id,
'metadata' => $metadata,
'subscription_data' => ['metadata' => $metadata], // renewals
]);
// Paddle.js, in a Blade view
Paddle.Checkout.open({ items: […], customData: @json(Rom::checkoutMetadata()) });
En tus tests: Rom::fake()
Como Mail::fake(): nada sale del test y puedes comprobar exactamente qué se habría enviado. También assertPageviewRecorded(), assertNothingTracked() y tracked('nombre') para ver el payload.
Para apagar runomine en los tests sin fingirlo, pon RUNOMINE_ENABLED=false en tu phpunit.xml.
use Runomine\Laravel\Facades\Rom;
public function test_signing_up_is_tracked(): void
{
$rom = Rom::fake(); // nothing leaves the test
$this->post('/register', ['email' => 'ana@example.com', …]);
$rom->assertTracked('signup');
$rom->assertTrackedTimes('signup', 1);
$rom->assertNotTracked('purchase');
}
Por qué no ralentiza tu app
-
Después de responder
Los eventos se guardan en memoria y se envían en lote cuando la respuesta ya ha salido, con un timeout de 2 segundos.
-
Nunca lanza excepciones
Ante un
5xxo un429reintenta dos veces con backoff; si aun así falla, descarta el lote en silencio. -
Sin dependencias
PHP 8.2+ con
ext-curlyext-json. Sin tablas ni migraciones en tu base de datos.
FAQ
Preguntas frecuentes
¿Qué versiones de Laravel y PHP soporta?
ext-curl y ext-json. Sin Laravel, el mismo paquete trae un cliente PHP independiente del framework.¿Funciona con Livewire e Inertia?
livewire/*) se excluyen por defecto. Las visitas de Inertia responden JSON y el middleware no las cuenta: para eso, añade el beacon, que sigue pushState.¿Guarda algo en mi base de datos o en el navegador?
Mi app está detrás de Cloudflare. ¿Algo especial?
$request->ip() sea la IP del visitante. Si no puedes, RUNOMINE_IP_HEADER=CF-Connecting-IP (solo entonces: esa cabecera es fácil de falsificar). El país se toma de CF-IPCountry. Las páginas que sirve la caché de Cloudflare no llegan a Laravel: añade el beacon en esas.¿Necesito un banner de cookies?
Sigue leyendo
- runomine frente a Google Analytics Qué ganas y qué pierdes al dejar GA4, y cómo cambiar sin quedarte a ciegas.
- runomine frente a Plausible Dos herramientas sin cookies. La diferencia: desde dónde se mide y cómo llega el dinero.
- Analítica server-side Qué es medir desde el servidor, qué ves y qué se te escapa, y cómo contar visitantes sin cookies.
Cuenta lo que importa.
Visitas, ventas y servidores en un solo sitio. Instalado en menos de lo que has tardado en leer esta página.