Saltar al contenido
runomine

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.

Terminal · .env
# 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 GET que responden 200 con text/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 tu admin/* en skip.
  • 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 · bootstrap/app.php
// 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().

Cualquier parte de tu app
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.

Tu controlador de checkout
// ['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.

tests/Feature/SignupTest.php
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 5xx o un 429 reintenta dos veces con backoff; si aun así falla, descarta el lote en silencio.

  • Sin dependencias

    PHP 8.2+ con ext-curl y ext-json. Sin tablas ni migraciones en tu base de datos.

FAQ

Preguntas frecuentes

¿Qué versiones de Laravel y PHP soporta?
Laravel 10 o superior y PHP 8.2+, con ext-curl y ext-json. Sin Laravel, el mismo paquete trae un cliente PHP independiente del framework.
¿Funciona con Livewire e Inertia?
La carga normal de cada página, sí. Las peticiones de actualización de Livewire (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?
No. El paquete no tiene migraciones ni tablas, y no pone cookies ni nada en el navegador. Los eventos van a runomine en lotes.
Mi app está detrás de Cloudflare. ¿Algo especial?
Configura los proxies de confianza para que $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?
Para visitas y embudos, runomine está diseñado para encajar en la exención de medición de audiencia (AEPD, CNIL). Atribuir ingresos a usuarios identificados va más allá y puede requerir consentimiento. Decídelo con tu DPO.

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.