Why Not Google Analytics? (Or why I love reinventing the wheel)
To be honest, this wasn't an easy call. I’ve been in development for quite a while and had grown accustomed to GA. What could be simpler: you slap a script with your ID onto the site, and data starts flowing into the analytics console. All that's left is to wait for the traffic to roll in and then analyze it by country, gender, age groups, and so on.
Yes, that’s how it used to be, but in today’s reality, there are objective reasons to rethink this concept. Let’s be real: hooking up Google Analytics in 2026 is like putting a massive deadbolt on your front door but leaving the keys with your neighbor. Everything seems under control, but the neighbor knows exactly when you came home, what you bought, and why you have a long face. And when someone visits you and wants to stay incognito, the neighbor won't give them the keys, and they won't even tell you they stopped by. Their response would be something like: “Nobody came, I never sleep, everything is under control…”. What am I getting at?
Remember the General Data Protection Regulation (GDPR)?
Now, for your analytics to work, you must display a cookie consent banner and get the user's permission. And that’s where the problem lies. 90% of users don’t accept all cookies—only the strictly necessary ones. And what does that mean? Google Analytics ends up dead in the water. Besides, everyone is sick and tired of these banners. So, if there’s a legal way to ditch them, why not take it?
(To learn how to avoid legal trouble with data protection laws, check out the article GDPR Without the Headache: A Guide for Web Developers in Germany )
The decision to drop GA was made. When I decided to hit the "Eject" button and catapult myself out of the Google ecosystem, I faced a logical question: what would fill the void? Because I still needed the numbers. I started looking at the heavy artillery.
The first candidate was Matomo (formerly Piwik): Probably the most powerful all-in-one machine. It’s like keeping a pet elephant in your backyard. It does everything, the database grows like crazy, but it requires a separate PHP server, MySQL, and constant babysitting. For my small pet projects, it felt like trying to drive nails with a microscope.
The second tool I looked at was Plausible / Fathom: Sleek, modern, and privacy-respecting. But there’s a catch: you either pay a subscription (a questionable investment for a free tool) or you mess around with self-hosted Docker versions, which also eat up a good chunk of your VPS RAM.
I looked at all of this and thought: “Do I really need to spin up an entire infrastructure just to know that 50 people read my article on German taxes yesterday?”. That’s when it hit me: I don’t need a "combine harvester." I need a tiny, precision scalpel that lives right inside my Laravel application.
I wanted something of my own: lightweight, like a morning espresso, and not asking annoying questions about GDPR. Plus, I was simply curious about who all these people (and bots) were "knocking" on my security tools at oleant.net. Since this blog also needed the same tool, I decided to develop a standalone package that could be easily installed via composer and published openly on Packagist.
For those interested in diving into the code, the package is called oleant/laravel-visit-analytics, compatible with Laravel versions 10/11/12. GitHub link: https://github.com/Oleant-NET/laravel-visit-analytics
The Heart of the System: Middleware and the Magic of the Aftertaste
Which architecture to use? There are various ways to implement this. Fortunately, Laravel has a great Middleware mechanism. I decided to stick with that, but would it slow down the user? They shouldn't have to care about my overhead. And what if the database goes down or something goes wrong? A 500 error page as the face of the site is definitely not what I was aiming for.
That’s why a crucial decision was made — to use the terminate() method. In a Laravel Middleware, it’s like a polite waiter: he brings you the check and smiles (the handle() method), and only after you’ve already left the restaurant does he go back to wipe the table and log the tip (the terminate() method).
The user has already received their page and is happy, while our server quietly and without rush writes the logs to the database in that moment. Even if something goes wrong, the client still leaves satisfied, and the waiter... just doesn't get a tip this time. Just kidding, but here’s how it works in practice:
PHP
public function terminate(Request $request, Response $response): void
{
try {
// Logic to exclude non-target clients, like Admin etc.
// …
// Next, write our visitors to the database
$this->logVisit($request);
} catch (\Throwable $e) {
// If the DB takes a nap, we won't wake the user with a 500 error
\Log::error("Analytics failed, but we're keeping our cool: " . $e->getMessage());
}
}
Lifehack: All analytics code must be wrapped in a try-catch. Believe me, there's nothing sillier than "crashing" an entire project just because the logger didn't have enough room for a long User-Agent or some other non-obvious case.
GDPR on the Fly: How Not to Become Public Enemy No. 1
To avoid slapping a banner the size of half the screen saying “We are watching you, bro,” I implemented IP anonymization. We simply trim the last part of the address before it ever touches the database. This anonymizes the user and fully complies with the law. Yet, we can still tell which country, data center, etc., the visit came from. We’ll talk more about data center bots once there's more data to show.
Back to IP anonymization:
PHP
// Before: 192.168.1.154 -> After: 192.168.1.0
It’s like seeing a crowd in masks: you understand that 5 people showed up, but who among them is your neighbor — you have no clue. The law is satisfied, and so is my conscience.
Payload: Gathering Only the Goodies
Initially, I wanted to write everything that comes in the URL to the database. But then I looked at the Livewire logs, where half the state of the planet is passed in the parameters, and realized — the database would explode. So, I decided to implement filtering based on allowed parameters in the config using array_intersect_key. Now, only what I’ve personally authorized in the config ends up in the logs. Clean, orderly, and zero fluff.
The default set in the package config looks like this:
PHP
'whitelist' => [
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'ref',
],
But you can, of course, change it by publishing the config in your project first:
Bash
php artisan vendor:publish --tag="visit-analytics-config"
After that, a visit-analytics.php file will appear in your config folder. There, you can not only expand the list of tracked parameters (for example, adding something like page or search) but also specify excluded paths so you don't turn your database into a dump for admin panel or technical endpoint logs.
What's Next?
The package started humming, and the data began to flow. I closed my laptop and went to bed, thinking I’d wake up to some visitor charts from a couple of friends. But reality turned out to be much tougher (and more interesting).
In the next episode, we’ll step through the "looking glass": who are Palo Alto Networks, why do bots read my Terms of Service in 7 seconds, and why Linux + DuckDuckGo is a badge of quality for a visitor?
Top comments (0)