rflex / analytics-laravel
Server-side product analytics (PostHog) for rFlex Laravel systems: events, identify/group, feature flags.
Requires
- php: ^7.3|^8.0
- ext-json: *
- guzzlehttp/guzzle: ^6.5|^7.0
- illuminate/contracts: ^7.0|^8.0|^9.0
- illuminate/http: ^7.0|^8.0|^9.0
- illuminate/queue: ^7.0|^8.0|^9.0
- illuminate/support: ^7.0|^8.0|^9.0
- psr/log: ^1.0|^2.0|^3.0
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^0.7|^1.0
- mockery/mockery: ^1.3
- orchestra/testbench: ^5.0|^6.0|^7.0
- phpcompatibility/php-compatibility: ^9.3
- phpunit/phpunit: ^9.0
- squizlabs/php_codesniffer: ^3.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Analytics de producto del lado servidor (PostHog) para los systems Laravel de rFlex: eventos,
identify/group y feature flags. Compatible con PHP 7.3 a 8.x y Laravel 7 a 9. No depende de
posthog/posthog-php (exige PHP 8 desde la v3); habla con la API HTTP de PostHog usando el cliente
HTTP de Laravel, por lo que en tests se fakea con Http::fake().
Para el navegador (GdP2 y webapp) existe el paquete hermano @rflex-spa/analytics
(packages/analytics-js). Ambos comparten el catálogo de eventos y el esquema de identidad.
Instalación
Hoy el paquete vive en starbase/packages/analytics-laravel. Cuando se publique en Packagist:
composer require rflex/analytics-laravel
php artisan vendor:publish --tag=analytics-config # opcional
El service provider y el alias Analytics se registran por auto-discovery.
Variables de entorno
| Variable | Default | Uso |
|---|---|---|
ANALYTICS_ENABLED |
false |
Interruptor general. Apagado: no se bufferiza, no hay HTTP ni jobs, los flags devuelven el default |
ANALYTICS_DRIVER |
posthog |
posthog, log (escribe los payloads al log, sin red) o null |
POSTHOG_API_KEY |
Token de proyecto (phc_...). Nunca una personal API key |
|
POSTHOG_HOST |
https://us.i.posthog.com |
https://eu.i.posthog.com para proyectos en la UE |
ANALYTICS_DELIVERY |
inline |
inline envía al terminar el request; queue despacha SendAnalyticsBatch |
ANALYTICS_QUEUE_CONNECTION / ANALYTICS_QUEUE |
null | Conexión y cola del job (null = las por defecto; sync permitido) |
ANALYTICS_FLAGS_ENABLED |
false |
Habilita la consulta de feature flags (está en el request path) |
ANALYTICS_FLAGS_TIMEOUT / ANALYTICS_FLAGS_CACHE_TTL |
0.3 / 60 |
Presupuesto en segundos y TTL del cache de flags |
ANALYTICS_SYSTEM / ANALYTICS_ENVIRONMENT / ANALYTICS_APP_VERSION |
Propiedades estándar que viajan en todo evento | |
ANALYTICS_LOG_CHANNEL |
null | Canal de log para el driver log y para los avisos de entrega |
El resto (batch.size, batch.max_buffer, sampling, catalog.strict, posthog.retries) se ajusta en
config/analytics.php.
Uso
use Rflex\Analytics\Catalog\Events; use Rflex\Analytics\Catalog\Props; use Rflex\Analytics\Facades\Analytics; use Rflex\Analytics\Identity\IdentityFactory; $identity = IdentityFactory::fromInstanceUser(clinica(), 'admin', $admin->idadmin, [ 'organization' => clinica(), ]); Analytics::identify($identity, ['email' => $admin->correo, 'role' => 'admin']); Analytics::group('organization', clinica(), ['name' => config('rflex.datosInstancia.nombre')]); Analytics::track(Events::REPORT_EXPORTED, [Props::REPORT => 'honorarios', Props::FORMAT => 'xlsx'], $identity); if (Analytics::isFeatureEnabled('nuevo-reporte', $identity)) { // ... }
Cada system bindea un IdentityResolver para que track() sin identidad explícita use al usuario
autenticado, y opcionalmente un EnablementResolver para combinar el env con una configuración por
tenant:
$this->app->bind(IdentityResolver::class, InstanceIdentityResolver::class); $this->app->bind(EnablementResolver::class, function () { return new class implements EnablementResolver { public function enabled(): bool { return (bool) config('analytics.enabled') && (bool) getConfig('general.habilitarAnalytics'); } }; });
Analytics::setContext(['client_code' => clinica()]) agrega propiedades a todos los eventos del proceso.
Esquema de identidad (compartido con el paquete JS)
| Actor | distinct_id |
Ejemplo |
|---|---|---|
| Usuario nominus | nominus:{nominus_user_id} |
nominus:42 |
| Admin de instancia (api, webapp, app sin nominus) | {client_code}:admin:{idadmin} |
clinicax:admin:17 |
| Trabajador de instancia | {client_code}:worker:{idusuario} |
clinicax:worker:9031 |
| Usuario presenza sin id nominus | presenza:{user_id} |
presenza:7 |
| Anónimo | uuid v4 con $process_person_profile: false |
Reglas: client_code en minúsculas y sin espacios; el rol del id es solo admin o worker (DT e
interno rflex van como person props); sin hash. Grupos: organization = código de organización
(clinica() en api, organizations.code en nominus y presenza), holding = uuid del holding. Person
props permitidas: email, nombre, rol, portal, plataforma, client_code, client_name. Nunca RUT.
Agregar un evento
- Constante en
Rflex\Analytics\Catalog\Events(snake_case, inglés) y la misma clave en el catálogo del paquete JS. - Nombres de propiedades en
Propssi son nuevos. - Si el volumen es alto, tasa en
config('analytics.sampling')yProps::SAMPLE_KEYcon el id de la entidad para que el muestreo sea determinista. El evento sale consample_ratepara reponderar. - Con
catalog.strict = true(recomendado enlocalytesting) los nombres fuera del catálogo lanzanInvalidArgumentException.
Entrega
| Situación | Qué pasa |
|---|---|
enabled=false |
track/identify/group/flush retornan sin tocar el resolver ni el buffer |
delivery=inline |
Se bufferiza y se envía al llegar a batch.size o en $app->terminating() (tras responder en php-fpm). En Vapor terminate() corre dentro de la invocación: usar queue |
delivery=queue |
Mismo buffer; al vaciarse se despacha SendAnalyticsBatch (arrays planos, tries = 1) a la conexión/cola configurada. La cola analytics hay que agregarla en Horizon, supervisord o vapor.yml |
| Comandos artisan | Flush al terminar el comando o al llenarse el buffer |
| Workers de cola | Flush después de cada JobProcessed |
El driver nunca lanza: los errores HTTP quedan en el log como warning. Si el buffer supera
batch.max_buffer se descartan los mensajes más antiguos y se avisa una vez.
Feature flags
POST /flags?v=2 con fallback a /decide?v=3; cache por identidad y grupos durante cache_ttl
segundos (sets incompletos no se cachean); cualquier error o timeout devuelve el default. flags.enabled
es independiente de los eventos porque la consulta bloquea el request. Para evaluación local a futuro,
bindear otra implementación de FlagEvaluator.
Demostrar que apagado se comporta igual
config(['analytics.enabled' => false]); Http::fake(); Queue::fake(); // ... ejercer el flujo ... Http::assertNothingSent(); Queue::assertNothingPushed();
Para ver los payloads sin red: ANALYTICS_ENABLED=true ANALYTICS_DRIVER=log.
En los tests de un system: $fake = Analytics::fake(); y luego assertTracked, assertNotTracked,
assertNothingTracked, assertIdentified, assertGrouped, setFlag.
Tests del paquete
Desde la raíz de starbase:
packages/analytics-laravel/bin/test-php73.sh # PHP 7.3.25 / Laravel 7 / Guzzle 6 (imagen api) packages/analytics-laravel/bin/test-php82.sh # PHP 8.2 / Laravel 9 (imagen presenza) packages/analytics-laravel/bin/lint-compat.sh # php -l + PHPCompatibility 7.3-
Cada script resuelve dependencias en un vendor propio (.vendor/php73, .vendor/php82) y no deja
composer.lock. Laravel 7, 8 y 9 tienen avisos de seguridad en Packagist; composer.json desactiva el
bloqueo por advisories solo para esta matriz de pruebas. La imagen api necesita systems/api/.env
para construirse.
Puntos de extensión
DriverFactory::extend('nombre', fn)para otro destino.EnablementResolverpara flags por tenant.FlagEvaluatorpara evaluación local.AnalyticsFakepara los tests de los systems.