How I Built a Funnel Analytics Engine with Laravel Horizon, Redis and a Dead-Simple REST API
Source: Dev.to
The Core Problem
Funnel analytics sounds simple: a user does A, then B, then C. What percentage make it from A to C? Where do they drop off?
In practice, it’s surprisingly tricky to build well:
- Events arrive asynchronously and out of order
- Funnels need to be flexible — different steps, different timeframes
- Calculation needs to be fast, even with thousands of events
- The integration overhead for the developer must be minimal
Goal: a developer should be able to start tracking in under 5 minutes with a single HTTP POST.
Architecture Overview
Browser/App → REST API → Event Storage → Queue → Funnel Engine → DashboardStack
- Backend: Laravel 12 with a modular architecture (
nwidart/laravel-modules) - Queue: Laravel Horizon + Redis
- Frontend: Next.js 14 + TypeScript
- Database: MariaDB
- Payments: Stripe
The API Layer
The integration is intentionally minimal:
POST /api/v1/events
X-OL-Tenant-Key: your-tenant-key
X-OL-App-Key: your-app-key
Content-Type: application/json
{
"event_name": "signup_completed",
"user_identifier": "user_123",
"metadata": {
"plan": "pro",
"source": "landing_page"
}
}Two headers, one JSON body—that’s the entire contract.
The controller validates the keys, identifies the tenant and tracked app, persists the event, and immediately returns 200. No heavy lifting in the request lifecycle.
The Funnel Engine
When a user builds a funnel in the dashboard — e.g. visited_pricing → started_trial → upgraded — the system must calculate conversion rates for each step.
All event writes dispatch a background job:
ProcessFunnelEngineJob::dispatch($event)->onQueue('funnels');The job:
- Picks up the event.
- Loads all funnels for the associated app.
- Recalculates conversion rates per step using a sliding‑window approach.
- Caches the results for the dashboard.
Why a queue?
- Performance: API responses stay fast regardless of how many funnels need recalculation.
- Resilience: Failed jobs retry automatically; no data loss on transient errors.
Laravel Horizon provides a real‑time dashboard to monitor job throughput, failures, and queue depth without extra infrastructure.
Multi‑Tenancy
Tracetics is multi‑tenant by design. Each tenant (company) can have multiple TrackedApps, each with its own events and funnels. The hierarchy:
Tenant
└── TrackedApp (X-OL-App-Key)
└── Events
└── Funnels
└── FunnelStepsAuthentication uses two headers:
X-OL-Tenant-Key— identifies the tenantX-OL-App-Key— identifies which app the event belongs to
This keeps the integration clean for developers while enforcing strict data isolation on the backend.
Plan Limits & Billing
Each subscription plan defines limits for TrackedApps, Funnels, and monthly Events. Limits are enforced at the controller level before any write operation:
if ($tenant->trackedApps()->count() >= $tenant->plan->app_limit) {
return response()->json(['error' => 'App limit reached'], 403);
}Stripe handles subscriptions via webhooks. A notable challenge: Stripe’s newer API moved current_period_end into items.data[0] rather than the subscription root object—a subtle breaking change that required an hour of debugging.
The TypeScript SDK
For developers who prefer typed clients over raw HTTP, a TypeScript SDK is available:
npm install tracetics-sdkimport { Tracetics } from 'tracetics-sdk';
const client = new Tracetics({
tenantKey: 'your-tenant-key',
trackedAppKey: 'your-app-key',
endpoint: 'https://tracetics.com'
});
await client.track({
event_name: 'signup_completed',
user_identifier: 'user_123'
});The SDK is built with tsup — dual ESM/CJS output, full TypeScript types, and zero dependencies.
What I Learned
Keep the write path dumb and fast.
Validate, persist, enqueue. Everything else belongs to the queue.Queue‑first architecture pays off immediately.
Funnel calculations never block API responses, and concerns stay well separated.Stripe webhooks are not optional.
Relying only on redirect‑based confirmation leads to unreliable subscription state.Multi‑tenancy is easier with a clear key hierarchy.
ExplicitTenant → App → Eventownership makes permission checking trivial and data isolation bulletproof.
What’s Next
Tracetics is live at with a free plan available. The TypeScript SDK is on npm as tracetics-sdk.
If you’ve built something similar or have questions about the architecture, feel free to share your thoughts in the comments.