Qué se mide
SYNTIweb registra 6 tipos de eventos para cada tenant:
- pageview → Visita a la landing
- click_whatsapp → Clic en botón de WhatsApp
- click_call → Clic en botón de llamada telefónica
- click_toggle_currency → Clic en toggle REF/Bs
- qr_scan → Escaneo del QR de tracking
- time_on_page → Tiempo de permanencia (cada 30 segundos)
El sistema NO usa cookies ni Google Analytics — todo se registra en la tabla analytics_events de la base de datos.
Tracking desde el frontend
La landing incluye un script JavaScript que envía eventos al backend vía POST:
// landing/templates/studio.blade.php:294
if (e.target.closest('a[href*="wa.me"]')) track('click_whatsapp');
if (e.target.closest('a[href^="tel:"]')) track('click_call');
Función track()
function track(eventType, metadata = {}) {
fetch('/api/analytics/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
tenant_id: {{ $tenant->id }},
event_type: eventType,
metadata: metadata
})
});
}
Endpoint de tracking
// AnalyticsController.php:23-88
public function track(Request $request): JsonResponse
{
$validated = $request->validate([
'tenant_id' => 'required|integer|exists:tenants,id',
'event_type' => 'required|string|in:pageview,click_whatsapp,click_call,click_toggle_currency,time_on_page,qr_scan',
'metadata' => 'nullable|array'
]);
$tenantId = $validated['tenant_id'];
$eventType = $validated['event_type'];
// Rate limiting: máx 100 eventos/minuto por tenant
$rateLimitKey = "analytics_rate_limit:{$tenantId}";
$currentCount = Cache::get($rateLimitKey, 0);
if ($currentCount >= 100) {
return response()->json(['success' => false, 'message' => 'Rate limit exceeded'], 429);
}
Cache::put($rateLimitKey, $currentCount + 1, now()->addMinute());
// Hash de IP (no guardar IP completa)
$ipHash = hash('sha256', $request->ip() . config('app.key'));
$now = now();
AnalyticsEvent::create([
'tenant_id' => $tenantId,
'event_type' => $eventType,
'user_ip' => substr($ipHash, 0, 45),
'user_agent' => $request->userAgent(),
'referer' => $request->header('referer'),
'event_date' => $now->toDateString(),
'event_hour' => (int) $now->format('H')
]);
return response()->json(['success' => true]);
}
Rate limiting
El sistema limita a 100 eventos por minuto por tenant para prevenir:
- Bots que disparan eventos falsos
- Scripts maliciosos
- Errores de loop infinito en el frontend
Estructura de la tabla analytics_events
CREATE TABLE analytics_events (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT NOT NULL,
event_type VARCHAR(50) NOT NULL,
reference_type VARCHAR(50) NULL,
reference_id INT NULL,
user_ip VARCHAR(45) NOT NULL, -- Hash de la IP
user_agent TEXT,
referer TEXT,
event_date DATE NOT NULL,
event_hour TINYINT NOT NULL, -- 0-23
created_at TIMESTAMP,
INDEX idx_tenant_date (tenant_id, event_date),
INDEX idx_tenant_type (tenant_id, event_type)
);
Los índices compuestos idx_tenant_date y idx_tenant_type optimizan las queries de analytics que filtran por tenant + fecha o tenant + tipo de evento.
Dashboard: métricas principales
El dashboard muestra estas métricas en tiempo real:
Visitantes únicos
// AnalyticsController.php:105-116
$visitorsToday = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_date', $today)
->where('event_type', 'pageview')
->distinct('user_ip')
->count('user_ip');
$visitorsWeek = AnalyticsEvent::where('tenant_id', $tenantId)
->whereBetween('event_date', [$weekAgo, $today])
->where('event_type', 'pageview')
->distinct('user_ip')
->count('user_ip');
Por qué distinct('user_ip'): Evita contar múltiples pageviews de la misma persona como visitantes distintos.
Clics en WhatsApp
// AnalyticsController.php:119-123
$whatsappClicks = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_type', 'click_whatsapp')
->whereBetween('event_date', [$weekAgo, $today])
->count();
Escaneos QR
// AnalyticsController.php:136-140
$qrScans = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_type', 'qr_scan')
->whereBetween('event_date', [$weekAgo, $today])
->count();
Tiempo promedio en página
// AnalyticsController.php:143-146
$avgTimeOnPage = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_type', 'time_on_page')
->whereBetween('event_date', [$weekAgo, $today])
->avg(DB::raw('1')) * 30; // Cada evento = 30 segundos
El frontend envía un evento time_on_page cada 30 segundos que el usuario permanece en la página.
Gráfico de últimos 7 días
// AnalyticsController.php:148-162
$last7Days = [];
for ($i = 6; $i >= 0; $i--) {
$date = now()->subDays($i)->toDateString();
$visitors = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_date', $date)
->where('event_type', 'pageview')
->distinct('user_ip')
->count('user_ip');
$last7Days[] = [
'date' => $date,
'visitors' => $visitors
];
}
El dashboard renderiza este array como gráfico de línea con Chart.js.
Endpoint para datos del día
// AnalyticsController.php:190-223
public function getToday(int $tenantId): JsonResponse
{
$today = now()->toDateString();
$visitors = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_date', $today)
->where('event_type', 'pageview')
->distinct('user_ip')->count('user_ip');
$whatsapp = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_date', $today)
->where('event_type', 'click_whatsapp')->count();
$qr = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_date', $today)
->where('event_type', 'qr_scan')->count();
$products = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_date', $today)
->where('event_type', 'product_click')->count();
return response()->json([
'success' => true,
'visitors_today' => $visitors,
'whatsapp_clicks' => $whatsapp,
'qr_scans' => $qr,
'products_viewed' => $products
]);
}
Este endpoint se llama cada 30 segundos desde el dashboard para actualizar métricas en vivo sin recargar.
Privacidad y GDPR
Hash de IP
$ipHash = hash('sha256', $request->ip() . config('app.key'));
$event->user_ip = substr($ipHash, 0, 45);
La IP real nunca se guarda. Se hashea con SHA-256 + salt del app key.
Por qué: Permite contar visitantes únicos sin almacenar datos personales identificables.
Sin cookies
El sistema no usa cookies ni localStorage — cada pageview se registra como nuevo evento.
Limitación: Si un usuario visita 3 veces en el día desde la misma red (misma IP hasheada), cuenta como 1 visitante único.
Tracking por referencia
La tabla incluye campos reference_type y reference_id para eventos específicos:
AnalyticsEvent::create([
'tenant_id' => $tenantId,
'event_type' => 'product_click',
'reference_type' => 'product',
'reference_id' => $productId,
// ...
]);
Uso: Medir qué productos específicos generan más clics.
Eventos por hora del día
El campo event_hour (0-23) permite analizar:
- ¿A qué hora hay más visitas?
- ¿Cuándo es mejor publicar ofertas?
// Ejemplo: heatmap de visitas por hora
$hourlyData = AnalyticsEvent::where('tenant_id', $tenantId)
->where('event_type', 'pageview')
->whereBetween('event_date', [$weekAgo, $today])
->select('event_hour', DB::raw('count(*) as total'))
->groupBy('event_hour')
->orderBy('event_hour')
->get();
La Fase F (futura) incluirá gráfico de heatmap por hora para identificar horarios pico.
Comparación con Google Analytics
| Feature | SYNTIweb Analytics | Google Analytics |
|---|
| Visitantes únicos | ✅ (por IP hash) | ✅ (por cookie) |
| Eventos personalizados | ✅ | ✅ |
| Tiempo en página | ✅ (30s intervals) | ✅ (exacto) |
| Embudo de conversión | ❌ (Fase F) | ✅ |
| Datos en tiempo real | ✅ | ✅ |
| Privacidad GDPR | ✅ (sin cookies) | ⚠️ (requiere banner) |
SYNTIweb analytics está optimizado para negocios venezolanos que necesitan métricas simples (visitantes, WhatsApp, QR) sin complicaciones legales.