<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oleksii Antoniuk</title>
    <description>The latest articles on DEV Community by Oleksii Antoniuk (@alantalex).</description>
    <link>https://dev.to/alantalex</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3865097%2Fe7eea4ef-83e8-438a-9701-865091bf4ec7.jpeg</url>
      <title>DEV Community: Oleksii Antoniuk</title>
      <link>https://dev.to/alantalex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alantalex"/>
    <language>en</language>
    <item>
      <title>Hunting Digital Chameleons: How We Defeated Botnets in Laravel v2.4.0</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Sat, 27 Jun 2026 07:30:00 +0000</pubDate>
      <link>https://dev.to/alantalex/hunting-digital-chameleons-how-we-defeated-botnets-in-laravel-v240-fha</link>
      <guid>https://dev.to/alantalex/hunting-digital-chameleons-how-we-defeated-botnets-in-laravel-v240-fha</guid>
      <description>&lt;p&gt;In the world of web traffic, there’s a simple rule: if it looks like a regular user, walks like a user, and even brings its favorite cookies along—it doesn't always mean there’s a human on the other side. Sometimes, it’s just a very diligent bot that happened to read the &lt;code&gt;User-Agent&lt;/code&gt; documentation yesterday.&lt;/p&gt;

&lt;p&gt;In this article, we’ll share how our traffic analysis tool evolved from naive trust in headers to a paranoid level of verification, and how that led to a "spring cleaning" of our architecture.&lt;/p&gt;

&lt;p&gt;(For more on the project's first deep refactoring, read our article: &lt;a href="https://oleant.dev/en/blog/refactoring-laravel-visit-analytics-the-path-to-version-200" rel="noopener noreferrer"&gt;Refactoring Laravel Visit Analytics: The Path to Version 2.0.0&lt;/a&gt; )&lt;/p&gt;

&lt;p&gt;Once upon a time, we were young and naive. We believed in the &lt;code&gt;User-Agent&lt;/code&gt; string with all our hearts. We looked at it like a passport: &lt;em&gt;"Oh, is that Chrome 128 on Windows 11? Welcome, honored user!"&lt;/em&gt; But the statistics from our VisitAnalytics package quickly knocked that romantic nonsense right out of us.&lt;/p&gt;

&lt;p&gt;We began to see strange patterns: thousands of "different" devices visiting the site, all with perfectly calibrated, "squeaky-clean" UA strings. But upon closer inspection, it turned out that the behavior of these "people" was suspiciously uniform. They were like soldiers in identical uniforms, marching through a desert where there was no one else but them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1: Bot vs. Reality – The Achilles' Heel
&lt;/h2&gt;

&lt;p&gt;We didn’t jump straight to active defense. At first, we just started collecting data. Our gut told us that not all users were who they seemed to be. Bots had evolved, learning to spoof their User-Agent strings so well that they were indistinguishable from real browsers. But they had an Achilles' heel: Client Hints (the  &lt;code&gt;Sec-CH-*&lt;/code&gt;  headers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Observing the logs, we realized a fundamental difference:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Humans don’t "optimize" headers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A real user's browser sends a whole bunch of  Sec-CH-*  headers automatically: from engine version to processor architecture. This is "living" information that changes along with updates. Furthermore, the  &lt;code&gt;"Accept-Language":"en-US,en;q=0.9,fr;q=0.8,es;q=0.7"&lt;/code&gt;  header of a normal human being differs from the bot equivalent  &lt;code&gt;"accept-language":"en-US,en;q=0.9"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bots are lazy or overthink it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Analyzing our package's statistics, we noticed: bot creators either forget about  &lt;code&gt;Sec-CH-*&lt;/code&gt;  entirely, leaving a void where a whole stack of data should be, or they "over-optimize" them. They try to generate them programmatically, leading to logical inconsistencies. It’s like a person in a tuxedo wearing rubber boots: individually, it’s all fine, but together, it makes you question the "tailor."&lt;/p&gt;

&lt;p&gt;Here are two examples from the log. The first is a typical human visit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2003:c1:d71c:fe1f::"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sec-ch-ua&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"\\\"&lt;/span&gt;&lt;span class="s2"&gt;Not)A;Brand&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;;v=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;8&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Chromium&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;;v=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;138&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Google Chrome&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;;v=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;138&lt;/span&gt;&lt;span class="se"&gt;\\\"\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sec-ch-ua-platform&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"\\\"&lt;/span&gt;&lt;span class="s2"&gt;Windows&lt;/span&gt;&lt;span class="se"&gt;\\\"\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sec-ch-ua-mobile&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;?0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sec-fetch-site&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sec-fetch-dest&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;document&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sec-fetch-mode&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;navigate&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;accept-language&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;en-US,en;q=0.9,fr;q=0.8,es;q=0.7&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;accept-encoding&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;gzip, br&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://oleant.dev/blog/freelancer-vertrage-fur-webentwickler-in-deutschland-so-schutzt-du-dich-rechtlich"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"referer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"www.google.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-25 10:10:03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anonymized_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-25 11:10:02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_official_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_reasons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;single_page_scan&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;single_page_scan&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;visit_depth&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;1_page_only&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;},&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;analyzed_at&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;2026-05-25 10:10:03&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-25 10:01:26.133"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing is that they didn't browse the site; they only read one article. Now, here is the second example, a clear bot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1235&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14.165.179.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;accept-encoding&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;gzip, br&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://oleant.dev/en/blog/how-to-write-a-resume-for-german-companies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"referer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-25 10:00:02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anonymized_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-25 11:00:02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_official_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_reasons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;suspicious_minimal_headers&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_accept-language&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_sec-fetch-dest&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_sec-fetch-site&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_sec-fetch-mode&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_sec-ch-ua&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_sec-ch-ua-platform&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;missing_mandatory_header_sec-ch-ua-mobile&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;suspicious_minimal_headers&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;found_count&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;required_count&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:5},&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;analyzed_at&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;2026-05-25 10:00:02&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-25 09:51:05.092"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "client" gave themselves away precisely because of the missing headers. But we didn't arrive at this realization immediately—let's look at how our understanding evolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: When Statistics Became Proof
&lt;/h2&gt;

&lt;p&gt;We began to notice that bots give themselves away through internal contradictions. For example, when a User-Agent claims to be Windows, but the  &lt;code&gt;Sec-CH-UA-Platform&lt;/code&gt;  headers timidly point to Android.&lt;/p&gt;

&lt;p&gt;At that moment, we realized: stop trusting the facade. Statistics showed us that for accurate identification, you shouldn't just read the headers, but look for cognitive dissonance within them. We stopped simply "recording" visits and started analyzing their integrity, turning our log files from a simple table into a real dossier on every "digital chameleon." This realization was the first step toward creating a system that later allowed us to move from passive observation to the effective hunting of botnets.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we tracked down botnets
&lt;/h3&gt;

&lt;p&gt;The User-Agent alone wasn't enough. We quickly understood that bots had learned to mimic virtuosically, swapping this string for any task. However, while observing the logs, we noticed a pattern: botnets often use proxies to rotate IP addresses, hoping to remain unnoticed. But they forget one detail—the "environment" of the request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stability in chaos
&lt;/h3&gt;

&lt;p&gt;We saw that despite constant IP changes (likely through proxy farms), the combination of &lt;code&gt;User-Agent&lt;/code&gt; + &lt;code&gt;Client Hints&lt;/code&gt; for bots is suspiciously stable. It's their signature. They can change their "face" (&lt;code&gt;IP&lt;/code&gt;), but their "digital skeleton" (&lt;code&gt;headers&lt;/code&gt;) remains unchanged for the entire network. To expose them, we created a Fingerprint: a unique hash that became our main weapon. In Laravel 11, we implemented this directly in the Middleware  TrackVisits  , turning a set of headers into a stable identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Hash generation: linking UA with critical headers&lt;/span&gt;
&lt;span class="nv"&gt;$targetHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;extractTargetHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Even if the IP changes, the hash content remains a constant for the botnet&lt;/span&gt;
&lt;span class="nv"&gt;$fingerprintInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$targetHeaders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$fingerprintHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$fingerprintInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hunting clusters
&lt;/h3&gt;

&lt;p&gt;When the same hash started appearing from 50,000 different IP addresses within an hour, we knew—this is a botnet. Previously, we stored this data in the  &lt;code&gt;botnet_fingerprints&lt;/code&gt;  table, but it quickly turned into a "graveyard" of useless records. We realized: we don't need an archive, we need real-time reactions. We rewrote the  &lt;code&gt;BotnetAnalyzer&lt;/code&gt;  to search for anomalies "on the fly," analyzing activity within the current window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Looking for anomalies in the current window without querying archive tables&lt;/span&gt;
&lt;span class="nv"&gt;$window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'analysis_window_minutes'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Looking for hash matches from different IP addresses&lt;/span&gt;
&lt;span class="nv"&gt;$isCluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VisitLog&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'fingerprint_hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fingerprint_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ip_address'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'!='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$isCluster&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The entire "pack" of bots is marked automatically at the moment of appearance&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markAsBotnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fingerprint_hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we removed  &lt;code&gt;botnet_fingerprints&lt;/code&gt;  from the database, the system accelerated instantly. We stopped hoarding the history of "dead" proxies and moved to detecting the botnet "conductor" by their handwriting. If hundreds of different IPs arrive with the same fingerprint—it doesn't matter how often they rotate proxies, we see it's one and the same "army."&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3: Cleaning the Augean Stables
&lt;/h2&gt;

&lt;p&gt;The hunt for the botnet started successfully, but our "digital trophy room" began to suffocate us. We were storing every suspicious hash in the  &lt;code&gt;botnet_fingerprints&lt;/code&gt;  table. With every passing day, it grew like yeast, turning from a security tool into a database bottleneck.&lt;/p&gt;

&lt;p&gt;We realized we had fallen into the "collection trap." We were trying to store attack history when, in reality, we only needed to know what was happening right this second. So, we took a radical step: we deleted  &lt;code&gt;botnet_fingerprints&lt;/code&gt;  from our database schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimization Results
&lt;/h3&gt;

&lt;p&gt;The outcome of our efforts exceeded all expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database load dropped by 40%&lt;/strong&gt;. Heavy JOINs and endless SELECTs on a table with millions of rows are gone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reaction speed&lt;/strong&gt;. Suspicion checks now happen almost instantaneously. Thanks to our first line of header analyzers, 95% of bots are filtered out before reaching more expensive checks (such as network-based PTR record lookups). All 11 analyzers are only passed by humans or as-yet-uncaught bots, which accounts for just a few percent. The rest get their "brand" marked by one of the analyzers in the check queue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear conscience&lt;/strong&gt;. We stopped being "archivists of evil" and became digital minimalists.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, our system no longer suffers from accumulated "digital baggage." It lives in the moment: it analyzes the request, compares it against "hot" patterns, and, if necessary, instantly flags the threat. We’ve learned that for botnet protection, it's not the depth of history that matters—it's the speed of decision-making here and now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 4: The Privacy Dilemma or "Who's There?"
&lt;/h2&gt;

&lt;p&gt;When we started hunting bots via hashes, we faced an ethical dilemma. That same  &lt;code&gt;fingerprint_hash&lt;/code&gt;  that helped us identify a botnet had essentially become a "digital footprint" of real people. If we hold a hash that can be decrypted or matched back to original headers, we are effectively storing personal data. And we are all about privacy!&lt;/p&gt;

&lt;p&gt;The goal became clear: we need to see botnet activity without seeing the identities of the users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anonymization as a standard in 2.4.0
&lt;/h3&gt;

&lt;p&gt;We have implemented the &lt;code&gt;FingerprintAnonymizerService&lt;/code&gt;. Its logic is simple: at the moment the log is saved, the system retains the data necessary for analytics, but which is ultimately too extensive for true anonymization. After the analytics, once we understand who is before us—human or bot—we no longer need their fingerprints. We pack the bot, along with its fingers and other parts, entirely into solitary confinement, while we welcome the worthy citizen to the site with full honors. All sensitive data is cleaned up in the process. The waiter (the web service) is, after all, not a policeman or security guard; if the guests are already at the disco, they are our guests, and their personal data no longer matters to us, we gladly serve them. But if it is a thief (read: bot), then the thief (aka bot) must sit in jail (a movie quote). He also gets his personal prison number, but without special amenities. And the bot John Johnson becomes simply inmate №245, I hope the analogy is clear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;VisitLog&lt;/span&gt; &lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$updates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="c1"&gt;// Transforming the complex User-Agent into a simple client "portrait"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'anonymize_ua'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$updates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_agent'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;anonymizeUserAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Replacing detailed headers with a simple list of keys&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'anonymize_headers'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$updates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'target_headers'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;anonymizeHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Wiping the original hash if analytics are complete&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'anonymize_fingerprint_hash'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$updates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'fingerprint_hash'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'anonym-sha256-ready'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$updates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Turning data into "information noise"
&lt;/h3&gt;

&lt;p&gt;The most interesting transformation happens inside  &lt;code&gt;anonymizeUserAgent&lt;/code&gt;. We no longer store the raw UA string. Instead, we use Client Hints (if available) to extract general parameters—browser, OS, and platform—and discard unique identifiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before anonymization:&lt;/strong&gt;&lt;br&gt;
We saw the specific engine version, processor architecture, and a full set of parameters that, combined, could "fingerprint" a unique user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After anonymization:&lt;/strong&gt;&lt;br&gt;
We see only Chrome / Windows (Desktop).&lt;br&gt;
We applied a similar approach to headers: the  &lt;code&gt;anonymizeHeaders&lt;/code&gt;  method simply returns an array of keys (&lt;code&gt;array_keys&lt;/code&gt;), stripping away any values that might contain cookies or specific session tokens. The result? Our logs now look like a set of "statistical generalizations." We still see that a botnet is attacking the site, and we still flag it in the system, but now we are fully protected against accusations of privacy violations. We transformed a detailed trace of every visitor's behavior into a safe stream of aggregated statistics.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Now, even if the log database falls into the wrong hands, it &amp;gt;would appear as cryptographic junk to anyone wanting to de-&amp;gt;anonymize our users. This is true engineering minimalism: &amp;gt;protecting the system without harming people.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion: Version 2.4.0 — From Detective to Professional System
&lt;/h2&gt;

&lt;p&gt;All these adventures—from the disappointment in the "honesty" of the User-Agent to the deletion of archive tables and the implementation of deep anonymization—culminated in release 2.4.0. This is not just a minor update. It is the transformation of our product from a "hobbyist detective" who simply keeps logs into a professional protection system that has learned the internet's most important lesson: you can't trust anyone, not even the headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bottom Line:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Performance.&lt;/strong&gt;&lt;br&gt;
Ditching unnecessary tables and switching to real-time analysis allowed us to reduce database load by &lt;strong&gt;40%&lt;/strong&gt; and stop worrying about log scalability issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy-First.&lt;/strong&gt;&lt;br&gt;
Thanks to the  &lt;code&gt;FingerprintAnonymizerService&lt;/code&gt;, we are no longer "secret keepers." We analyze threat patterns while leaving users' personal data off our analyzer's radar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart Detection.&lt;/strong&gt;&lt;br&gt;
We now catch botnets not by IP, but by their "digital signature," making proxy rotation attempts meaningless.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next?
&lt;/h3&gt;

&lt;p&gt;We continue to evolve. We are already looking into implementing Bloom filters for even lightning-fast, on-the-fly verification of "suspicious" fingerprints. Regarding the interface, we have postponed plans for deep integration with Filament for now. Yes, we want to see beautiful dashboards and attack graphs right in the Laravel admin panel, but our current priority is maximum data purity and detection accuracy. Filament is the storefront, but we are still organizing the "warehouse." But rest assured: botnet visualization in the admin panel is only a matter of time, and this item is bolded at the top of our backlog, waiting for its turn.&lt;/p&gt;

&lt;p&gt;Version 2.4.0 is the foundation. We have learned to see the invisible and have cleared the system of excess "digital trash." Onward—faster, bigger, and more efficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay tuned, we’re still on the hunt.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>analytics</category>
      <category>laravel</category>
      <category>backend</category>
    </item>
    <item>
      <title>Human-Centric Bot Detection: Lessons from Sony Ericsson and Win98</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/alantalex/human-centric-bot-detection-lessons-from-sony-ericsson-and-win98-4bkh</link>
      <guid>https://dev.to/alantalex/human-centric-bot-detection-lessons-from-sony-ericsson-and-win98-4bkh</guid>
      <description>&lt;p&gt;In my previous article &lt;a href="https://oleant.dev/en/blog/refactoring-laravel-visit-analytics-the-path-to-version-200" rel="noopener noreferrer"&gt;"Refactoring Laravel Visit Analytics: The Path to Version 2.0.0"&lt;/a&gt; we took a deep dive into how I rebuilt the "engine" of my analytics. We replaced a bulky Middleware with an elegant chain of analyzers and taught the database to value every single millisecond. But as we know, any architecture is just a skeleton. The real adventure began when I started feeling like a "digital demiurge" and began cranking up the detection rules, turning my package into an uncompromising and very stern sheriff.&lt;/p&gt;

&lt;p&gt;You know that feeling when you get a powerful tool in your hands and you want to take everything to the absolute extreme? I got carried away: blocking everything that smelled like "legacy," penalizing the slightest header mismatch, and purging any anomalies with a red-hot iron. At some point, my "sheriff" became so suspicious that he started eyeing even law-abiding citizens.&lt;/p&gt;

&lt;p&gt;Today, I’ll tell the story of how excessive strictness and a belief in the "ideal internet" almost turned my site into a closed club for an elite few owning the latest iPhones. We’ll analyze two real visits from the past that forced me to hit the brakes and completely rethink the very philosophy of Bot Score. This is a tale of how I taught my code to distinguish a cunning bot from a real human being stuck in the digital Paleozoic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: Dictatorship and the Vietnamese "Ghost" on Windows 98
&lt;/h2&gt;

&lt;p&gt;It all started when I decided to tighten the screws as much as possible. Windows XP, Windows 2000, and old versions of IE went straight to the penalty list in my config. I was full of youthful (adjusted for experience) maximalism: in 2026, no one in their right mind would be surfing the web on 20-year-old hardware. I was certain I had set the traps perfectly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And then, a guest from Vietnam pops up in the logs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first, the system went into a stupor. My blacklist had XP, but I simply forgot that Windows 98 even existed! It was a "blind flight"—the bot was using a system so ancient it was off the radar of my primary settings. However, the updated logic of version 2.0.0 and the analyzer pipeline didn't fail. Even without finding a direct ban on Win98, the system cross-referenced the fossilized OS and the dead browser.&lt;/p&gt;

&lt;p&gt;The result? The cumulative weight of evidence broke through the psychological barrier. Here’s what that interrogation protocol looked like in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14.177.144.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Opera/8.15 (Windows 98; fi-FI) Presto/2.9.180 Version/12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://oleant.dev/uk/blog/mii-texnicnii-stek-iak-stvoriuvati-sucasni-ta-funkcionalni-vebsaiti"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"70"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_reasons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"obsolete_os"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"obsolete_browsers"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"os_signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Windows 98"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"browsers_signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Opera/8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"analyzed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-09 17:46:14"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Despite the "intelligent" behavior—the guest from Vietnam arrived via a direct link and was supposedly thoughtfully reading an article in Ukrainian about my tech stack—the verdict was harsh: Bot.&lt;/p&gt;

&lt;p&gt;In 2026, the probability of a living person reading an IT blog via Opera 8.15 running on Windows 98 is near zero. It’s either a very specific script or an old botnet that hasn't been updated for so long it turned into a digital museum exhibit itself. My "digital customs" worked: the guest was classified as a bot, and their visit was annulled in the statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: My Son, a Sony Ericsson, and the "Amnesty"
&lt;/h2&gt;

&lt;p&gt;My faith in the infallibility of rigid algorithms finally crumbled when my son visited the site. As a connoisseur of tech-retro, he decided to stress-test my blog using a rare Sony Ericsson running Opera Mini.&lt;/p&gt;

&lt;p&gt;For a system I had just "loaded up" to fight digital pests, this visit looked like a typical clone attack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strange IP:&lt;/strong&gt; The request came through Opera proxy servers (Germany, Teledata), which is often characteristic of botnets.&lt;br&gt;
&lt;strong&gt;Missing Referer:&lt;/strong&gt; Old mobile browsers saved every byte and often didn't transmit the referer header.&lt;br&gt;
&lt;strong&gt;Suspicious Signature:&lt;/strong&gt; J2ME/MIDP in 2026? To the code, this sounds like "I'm a Python script pretending to be a grandfather."&lt;br&gt;
But when I opened the log, I didn't see a ban; I saw a "yellow card":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"185.231.75.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Opera/9.80 (J2ME/MIDP; Opera Mini/8.0.35626/191.396; U; de) Presto/2.12.423 Version/12.16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_bot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_reasons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"missing_referer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single_page_scan"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ptr_record"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"host-185-231-75-1.teledata-fttx.de"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"visit_depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1_page_only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"referer_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct_navigation"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The score froze at the 50 mark. The system "squinted," noted the missing referer and the strange source, but didn't cross the "Rubicon" of 70 points. Why? Because in version 2.0.0, I implemented a "mercy coefficient" by separating the weights.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works in the config:
&lt;/h3&gt;

&lt;p&gt;I’ve separated ancient browsers into a category of "functionally extinct," but assigned them a moderate penalty. Take a look at this configuration snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * List of browsers considered "extinct" for humans in 2026.
 * MSIE, old Opera on the Presto engine — these are often used by cheap parsers.
 */&lt;/span&gt;
&lt;span class="s1"&gt;'target_browsers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'MSIE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Internet Explorer 10 and below&lt;/span&gt;
    &lt;span class="s1"&gt;'Trident/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Internet Explorer 11&lt;/span&gt;
    &lt;span class="s1"&gt;'Opera/8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// Ancient Opera&lt;/span&gt;
    &lt;span class="s1"&gt;'Opera/9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="s1"&gt;'weights'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * Penalty for fossilized OS and browser. 
     * Combined (35+35), they hit 70 — the bot detection threshold.
     */&lt;/span&gt;
    &lt;span class="s1"&gt;'obsolete_os'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'obsolete_browsers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Fine Line Between "Retro" and "Bot"
&lt;/h3&gt;

&lt;p&gt;This approach is exactly what saved my son's visit. His User-Agent showed Opera/9.80, which didn't fall under the rigid Opera/8 or Opera/9 filter. The system realized: we are looking at something very old (J2ME), but it’s not on my list of "guaranteed dead."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;BANNED&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Vietnamese bot (Win 98 + Opera 8.15)&lt;/em&gt; received 35 for the OS and 35 for the browser. Total: 70. BANNED.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PASSED&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Son on Sony Ericsson (J2ME + Opera 9.80)&lt;/em&gt; received a penalty for missing referer and suspicious activity, but only scored 50. PASSED.&lt;/p&gt;

&lt;p&gt;This is humanity expressed in code. We don't just strike blindly. We give the "ghosts of the past" a chance if they behave decently. But the moment a retro gadget decides to scan your ports — the weight sum will instantly send it to the ban list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Smart Filters and "Anomaly" Logic
&lt;/h2&gt;

&lt;p&gt;These two cases helped me calibrate the "threshold of pain." In version 2.0.0, the analyzers stopped being mere penalty calculators — they now engage in a meaningful dialogue.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Contextual Verification (Mitigation)
&lt;/h3&gt;

&lt;p&gt;We still penalize for an old OS, but the system now understands historical context. If the browser matches its era (like Opera Mini on a Sony Ericsson), the penalty will be moderate. This allows retro-geeks and owners of niche gadgets to remain in the "green zone" (up to 70 points).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Anomaly Detector (Instant Ban)
&lt;/h3&gt;

&lt;p&gt;However, there are things the system never forgives. I call this "digital schizophrenia." These are cases where the User-Agent header tries to sit on two chairs from different decades.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The "Guest from the Future" Anomaly:&lt;/strong&gt; When Windows NT 10.0 (modern Windows 10) pops up in the logs, but the browser claims to be MSIE 6.0 or an old Opera 8. In 2026, no one is going to install a 20-year-old browser on modern hardware that can't even render Google properly. This is a 100% bot mimic that simply grabbed an old preset and forgot to update the OS string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Artifact" Anomaly:&lt;/strong&gt; The reverse situation — modern Chrome 140+ on Windows NT 6.1 (Windows 7). Browser developers ended support for "7" long ago, and seeing a fresh build there is a sure sign that a script is trying to appear "modern" while using an old server config.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. The 70-Point Threshold: Our "Rubicon"
&lt;/h3&gt;

&lt;p&gt;For everyone else, 50 points is a "yellow card." We observe, we record evidence, but we don't close the door. This gives people with specific privacy settings or rare software a chance to remain legitimate guests. But should such a guest show the slightest aggression — the threshold is instantly breached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue: Data Purity vs. Digital Inquisition
&lt;/h2&gt;

&lt;p&gt;Developing &lt;code&gt;laravel-visit-analytics&lt;/code&gt; forced me to look beyond just writing code. It taught me an important rule: behind every IP address, behind every strange User-Agent string, there is a story. Whether it's an old scout-bot "cobbled together" somewhere in Hanoi, or my son deciding to breathe life into a vintage Sony Ericsson.&lt;/p&gt;

&lt;p&gt;My goal in version 2.0.0 isn't to build a perfect digital prison with a checkpoint at every turn. My goal is to create a smart, contextual filter. One that ruthlessly sifts through noise and trash but leaves the door slightly ajar for exceptions. Because it is in those exceptions — the people with old phones, the retro-enthusiasts, and the privacy paranoids — that the spirit of the real, living internet we once fell in love with still lingers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;The foundation is laid; the architecture has been battle-tested against real bots and relatives. Now, the main task is to make this data visual. I plan to move towards visualization so that all these "evidences" and "suspicion scores" turn from dry JSON strings into clear charts and dashboards. Most likely, Laravel Filament will be my faithful companion in this, but in the world of development, there is always room for surprises and new challenges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;So stay tuned — there's plenty more to explore "under the microscope"!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>laravel</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Refactoring Laravel Visit Analytics: The Path to Version 2.0.0</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Thu, 11 Jun 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/alantalex/refactoring-laravel-visit-analytics-the-path-to-version-200-1e72</link>
      <guid>https://dev.to/alantalex/refactoring-laravel-visit-analytics-the-path-to-version-200-1e72</guid>
      <description>&lt;p&gt;Remember my recent guest with Firefox 140.0, the one I mentioned in &lt;a href="https://oleant.dev/en/blog/a-lie-detector-for-http-requests-analytics-through-time" rel="noopener noreferrer"&gt;"A Lie Detector for HTTP Requests: Analytics Through Time"&lt;/a&gt;? That case proved it: the "digital customs" is working, and the scoring system is diligently building "suspicion dossiers" on every shady visitor.&lt;/p&gt;

&lt;p&gt;But here’s the catch: the smarter my "lie detector" got, the harder it became for it to breathe. In version 1.3.0, I added port checks, referrer loop detection, and the infamous &lt;em&gt;Snowball Effect&lt;/em&gt;. At some point, I looked at the main Middleware code and was horrified. It started to resemble a Swiss Army knife that some fanatic tried to upgrade with a chainsaw, a microscope, and a fishing net all at once.&lt;/p&gt;

&lt;p&gt;The logic branched out, conditions multiplied, and milliseconds began crowding the logs, demanding surgical precision. I realized that if I left it as is, the package itself would turn into a sluggish "bot," spending more resources on self-analysis than on serving actual guests.&lt;/p&gt;

&lt;p&gt;Today, I’m going to tell you why my &lt;code&gt;laravel-visit-analytics&lt;/code&gt; package suddenly skipped several steps — jumping from version 1.3.0 straight to 2.0.0. Spoiler: we performed full-scale "open-heart surgery" on the project. We didn't just repaint the facade; we completely rewrote the architecture so the system could scale indefinitely without losing a fraction of a second or a shred of common sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jumping the Chasm: Why SemVer is About Honesty
&lt;/h2&gt;

&lt;p&gt;In the dev world, there’s an unwritten code of honor — SemVer (Semantic Versioning). It’s simple but rigid math: &lt;strong&gt;MAJOR.MINOR.PATCH&lt;/strong&gt;. If you just fixed a typo, that’s a patch. If you added a new feature, that’s minor. But if your changes make a user "scratch their head" over a crashed database — you are obligated to bump the first digit.&lt;/p&gt;

&lt;p&gt;I could have spent ages patching version 1.x, trying to maintain backward compatibility, but the old foundation simply couldn't support the weight of new ideas. Version 2.0.0 is my way of saying: "We swapped the chassis while driving. Please, check the manual!"&lt;/p&gt;

&lt;p&gt;The main stumbling block was in the migrations. Previously, the log table structure was standard and fairly relaxed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Version 1.3.0: Standard timestamps, basic indices&lt;/span&gt;
&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'visit_logs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Second-level precision&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In version 2.0.0, we switched to "High Definition." For the new analyzers to work and for the Snowball Effect to trigger accurately, we needed microsecond precision and additional fields to store "evidence."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Version 2.0.0: Microseconds and flexible data structure&lt;/span&gt;
&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'visit_logs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="c1"&gt;// Moving to milliseconds for Laravel + MySQL/MariaDB&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I had released this as 1.4.0, an automatic composer update could have turned some developer's morning into a nightmare with &lt;code&gt;Column not found&lt;/code&gt; errors or data type mismatches. A major release isn't about hype; it's a bulletproof vest for your data. We consciously chose this break to lay the groundwork for the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Vanishing Milliseconds" Problem and Moody SQLite
&lt;/h2&gt;

&lt;p&gt;The first serious blow came from the database world. My stack is pretty standard: powerful MariaDB in production, and lightning-fast in-memory SQLite for testing. And it was exactly at the intersection of these two worlds that the "mystical" bugs began.&lt;/p&gt;

&lt;p&gt;Picture this: an elite bot makes a series of requests. In reality, there are fractions of a second between them — say, 200 or 300 milliseconds. But standard &lt;code&gt;$table-&amp;gt;timestamps()&lt;/code&gt; in Laravel create columns with whole-second precision by default.&lt;/p&gt;

&lt;p&gt;As a result, for the database, three different visits occurring at &lt;code&gt;14:00:00.100&lt;/code&gt;, &lt;code&gt;14:00:00.400&lt;/code&gt;, and &lt;code&gt;14:00:00.800&lt;/code&gt; merged into one blurry "ecstasy" timestamped 14:00:00. This completely broke the Snowball Effect logic: the system couldn't build a chain of events because, from its perspective, they happened simultaneously.&lt;/p&gt;

&lt;p&gt;To restore the analytics' "vision," I had to switch to &lt;code&gt;dateTime('created_at', 3)&lt;/code&gt;. Those "three decimal places" became my entry ticket to the world of major updates. But SQLite, that moody little engine, started to resist. It simply ignored the precision setting in migrations unless you approached it with "special care."&lt;/p&gt;

&lt;p&gt;I had to implement a driver check directly in the migration code to ensure the package remained universal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// For MySQL/MariaDB we explicitly set precision, &lt;/span&gt;
&lt;span class="c1"&gt;// for SQLite we use the standard as it stores dates as strings&lt;/span&gt;
&lt;span class="nv"&gt;$driverName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDriverName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'visit_logs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$driverName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$driverName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'sqlite'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$precision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is this critical? Without this fix, my Pest tests were becoming a lottery. The analyzer saw three records with identical times and couldn't tell which came first. Now, the precision is surgical — we see every "sneeze" from a bot with millisecond accuracy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lesson learned: If your analytics can't see milliseconds, it's &amp;gt;blind. Bots will appear to you as either supernaturally fast or &amp;gt;magically synchronous. В modern web security, a second is an &amp;gt;eternity — enough time to hack half a website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From "Layer Cake" to Analyzer Pipeline
&lt;/h2&gt;

&lt;p&gt;In version 1.3, the entire interrogation logic—from hunting for "traitorous ports" to calculating "referrer loops"—lived inside a single, massive class. It felt like a detective’s office in a low-budget TV show: evidence, protocols, yesterday’s sandwiches, and personal files were all piled onto one desk. Add one new check, and the whole shaky structure threatened to collapse on your head.&lt;/p&gt;

&lt;p&gt;In 2.0.0, we did a deep clean and implemented the Chain of Responsibility pattern. Now, each "suspicion zone" is handled by its own highly specialized agent—the Analyzer.&lt;/p&gt;

&lt;h3&gt;
  
  
  How this "Special Task Force" works:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AnalysisState:&lt;/strong&gt; This is our "Case File." A special container object that is carefully passed through the entire chain. Each analyzer records its findings there: scores, reasons, and indisputable evidence.&lt;br&gt;
&lt;strong&gt;BotAnalysisService:&lt;/strong&gt; The "Police Chief" managing the process. It takes the request and hands it over to the profile experts one by one.&lt;/p&gt;
&lt;h3&gt;
  
  
  Meet the "staff" in version 2.0.0:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ExplicitBotsAnalyzer:&lt;/strong&gt; Deals with those who honestly (or foolishly) admit in the headers that they are bots.&lt;br&gt;
HeaderIntegrityAnalyzer: Checks header integrity. If a browser claims to be Chrome but acts like a leaky bucket, it’s his client.&lt;br&gt;
&lt;strong&gt;NetworkAnalyzer:&lt;/strong&gt; The tough guy checking IP "residency" (data centers, clouds, suspicious subnets).&lt;br&gt;
ObsoleteOSAnalyzer &amp;amp; OutdatedBrowserAnalyzer: The "Archeologist" duo. They track down those coming from the "Digital Paleozoic" (Hello, Windows XP!).&lt;br&gt;
&lt;strong&gt;RefererAnalyzer:&lt;/strong&gt; Specialist in "loops" and suspicious ports.&lt;br&gt;
&lt;strong&gt;ReputationAnalyzer:&lt;/strong&gt; Cross-references the guest against blacklists and past sins.&lt;br&gt;
&lt;strong&gt;HoneypotAnalyzer:&lt;/strong&gt; Our expert in "honey traps." If someone pokes around /.env, this analyzer closes the case.&lt;/p&gt;

&lt;p&gt;Why is this great? This architecture provides isolation and clarity. Each analyzer is an independent file of a couple hundred lines, easy to test and extend. If tomorrow hackers release a new generation of bots imitating, say, "smart fridges," I won't need to rewrite the system core. I'll just create a &lt;code&gt;SmartFridgeAnalyzer.php&lt;/code&gt;, drop it into the chain, and my "Digital Sheriff" will instantly learn to recognize threats from household appliances.&lt;/p&gt;

&lt;p&gt;Behind the scenes, everything is managed by &lt;code&gt;RetroAnalysisService&lt;/code&gt; — our "Cold Case Unit" that implements the Snowball Effect, cleaning up visit history if a guest finally slips up at a later stage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Control Panel: How the "Engine" Works
&lt;/h2&gt;

&lt;p&gt;Architecture isn't just about beautiful classes; it's about the ease of managing them. In version 2.0.0, I wanted the user to be able to disable heavy checks or add their own "on the fly" without digging into the package source. All control is centered in the configuration file. Each analyzer is an independent block that can be toggled with a single switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/visit-analytics.php&lt;/span&gt;
&lt;span class="s1"&gt;'analyzers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'obsolete_os'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'enabled'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'class'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\Oleant\VisitAnalytics\Analyzers\ObsoleteOSAnalyzer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'params'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'target_os'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Windows NT 5.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Windows NT 6.0'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="c1"&gt;// ... other settings&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the "magic" inside the &lt;code&gt;BotAnalysisService&lt;/code&gt;. It doesn't just iterate through classes; it works like a smart pipeline with built-in fail-safes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$analyzers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Check if the expert is active&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'enabled'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. Injection via Laravel container (Dependency Injection in action!)&lt;/span&gt;
        &lt;span class="cd"&gt;/** @var BotAnalyzerInterface $analyzer */&lt;/span&gt;
        &lt;span class="nv"&gt;$analyzer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Pass only the required parameters (Encapsulation)&lt;/span&gt;
        &lt;span class="nv"&gt;$analyzer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'params'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 4. "Fail-safe" mode: if one agent fails, the investigation continues&lt;/span&gt;
        &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addEvidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'execution_errors'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'analyzer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'class'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'error'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this matters:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Parameter Isolation:&lt;/strong&gt; Each analyzer receives only its own portion of settings. It doesn't know about others and can't interfere.&lt;br&gt;
&lt;strong&gt;Fault Tolerance:&lt;/strong&gt; If an error occurs in one analyzer, the main process won't break. The system logs the "evidence" of the failure and moves on with the interrogation.&lt;br&gt;
&lt;strong&gt;Clean Code via DI:&lt;/strong&gt; Using &lt;code&gt;app($settings['class'])&lt;/code&gt; allows you to use any other Laravel services in your analyzer constructors.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code That Doesn't Cover Its Tracks: Collecting "Digital Evidence"
&lt;/h2&gt;

&lt;p&gt;Remember how in version 1.3 we just recorded suspicions? In 2.0, I realized we needed a full-blown crime scene investigation protocol. But a purely technical problem arose. Previously, the method for adding evidence was too simplistic. If &lt;code&gt;ObsoleteOSAnalyzer&lt;/code&gt; found an old Windows version, and then &lt;code&gt;OutdatedBrowserAnalyzer&lt;/code&gt; found an ancient Internet Explorer, they might accidentally "fight" over keys in the data array.&lt;/p&gt;

&lt;p&gt;As a result, the latest piece of evidence would simply overwrite the previous one. PHP arrays are powerful, but with a careless &lt;code&gt;array_merge&lt;/code&gt;, they turn into an eraser that wipes out history. In version 2.0, I rewrote the evidence collection logic in &lt;code&gt;AnalysisState&lt;/code&gt;. Now we use "smart merging" (recursive merging) that doesn't overwrite data but neatly stacks it up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AnalysisState.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addEvidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;self&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If data already exists for this key, we don't overwrite it; &lt;/span&gt;
    &lt;span class="c1"&gt;// we turn it into a list or supplement the array.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Forensic Report in Your Logs
&lt;/h2&gt;

&lt;p&gt;Thanks to this "thrifty" approach, the database now stores a detailed dossier instead of an abstract status. When you open the visit_logs table, the evidence field (which is now a full-fledged JSON) tells a whole story:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bot Score:&lt;/strong&gt; 85 (Trust threshold exceeded)&lt;br&gt;
&lt;strong&gt;Reasons:&lt;/strong&gt; ['obsolete_os', 'obsolete_browsers', 'missing_referer']&lt;br&gt;
&lt;strong&gt;Evidence:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"os_signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Windows NT 5.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"browsers_signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MSIE 6.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"referer_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked_analyzers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ObsoleteOSAnalyzer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OutdatedBrowserAnalyzer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RefererAnalyzer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach turns analytics into a tool for evidence-based security. If a client comes to you asking, "Why was I blocked?", you won't mumble about "algorithms"—you'll show concrete facts: "Your browser from 2001 and the lack of a referrer resulted in a suspicion score of 85." This is the transparency I aimed for when moving to version 2.0.0. We are no longer just guessing—we are documenting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue: Why Did We Do This?
&lt;/h2&gt;

&lt;p&gt;Refactoring is a tricky business. At first glance, it seems like a boring and thankless task: you spend dozens of hours to end up with... exactly the same application, which just works "more correctly" under the hood. No new buttons, no flashy animations.&lt;/p&gt;

&lt;p&gt;But in reality, it’s the only way to keep a project from being buried under the weight of its own code. If I had continued "patching" logic into the version 1.3 Middleware, any attempt to add a new check would have soon turned into defusing a bomb. One wrong if—and the whole analytics system goes down.&lt;/p&gt;

&lt;p&gt;Version 2.0.0 is my "Engineering Manifesto." By laying down a clean analyzer architecture, I’ve prepared the perfect runway for the most ambitious stage of the project — visualization in Filament. Now that we have detailed evidence in JSON and a clear bot_score, turning these dry numbers into interactive graphs, threat maps, and real-time dashboards is only a matter of time.&lt;/p&gt;

&lt;p&gt;But not everything in the world of algorithms obeys the dry logic of ones and zeros. Sometimes, even the most perfect architecture falters before... an ordinary old phone. In the next article, I’ll tell an almost detective-like story of how my own son nearly became an "enemy of the system" by visiting the site from a vintage Sony Ericsson. We’ll explore how to avoid turning your defense into a digital dictator and why sometimes you need to let a "suspicious" guest through.&lt;/p&gt;

&lt;p&gt;Stay tuned—the most interesting things are always hidden in the details!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>analytics</category>
      <category>webdev</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>A Lie Detector for HTTP Requests: Analytics Through Time</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Wed, 03 Jun 2026 03:00:00 +0000</pubDate>
      <link>https://dev.to/alantalex/a-lie-detector-for-http-requests-analytics-through-time-2b43</link>
      <guid>https://dev.to/alantalex/a-lie-detector-for-http-requests-analytics-through-time-2b43</guid>
      <description>&lt;p&gt;Remember that guy from the last article who visited my site using Firefox 140.0 while the rest of the world was still stuck in the 130s? Back then, it felt like an exotic anomaly, a visit from a "guest from the future." But when these "time travelers" start showing up in formation, you realize: it's time to stop being surprised and start classifying them.&lt;/p&gt;

&lt;p&gt;(Read the details in the article on my Blog &lt;a href="https://oleant.dev/en/blog/through-the-looking-glass-of-logs-karachi-police-duckduckgo-and-ipv6-magic" rel="noopener noreferrer"&gt;"Through the Looking Glass of Logs: Karachi Police, DuckDuckGo, and IPv6 Magic"&lt;/a&gt; )&lt;/p&gt;

&lt;p&gt;If the first version of my Laravel analytics package was just a "peep-hole" in the door, version 1.3.0 has evolved into a full-scale digital customs checkpoint—complete with X-rays, biometrics, and an elephant's memory. Let’s look under the hood and break down how to distinguish a real human from a script that desperately wants to be your friend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1: The Evolution of Paranoia (Scoring System)
&lt;/h2&gt;

&lt;p&gt;In the beginning, I was naive. I believed the internet was a world of black and white: if the User-Agent said "Googlebot," it was a robot—we’d shake its hand and show it to the indexing section. If it said "Mozilla/5.0," it was a human—we’d pour them a coffee and log them into the "Visitors" table.&lt;/p&gt;

&lt;p&gt;But the reality of 2026 quickly shattered those rose-colored glasses. Today, even the laziest spam-bot, whipped up by a high schooler on a weekend, can mimic the latest Chrome build so skillfully that standard verification methods just give up. The bot no longer screams "I am a robot!". It enters quietly, subtly, rubbing its virtual palms together.&lt;/p&gt;

&lt;p&gt;Realizing this, I turned my package into a digital investigator that no longer makes snap judgments. Now, it uses a weighting system (Scoring). The package doesn't issue a verdict immediately—it observes, cross-references facts, and builds a "suspicion file."&lt;/p&gt;

&lt;h3&gt;
  
  
  Here is what this real-time interrogation protocol looks like:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Appeared out of nowhere" (+35 points):&lt;/strong&gt; Imagine someone materializing right in the middle of your kitchen, bypassing the front door. If a request knocks on an internal page (e.g., straight to /checkout or deep into the blog) without a Referer header, my inner detective narrows his eyes. Regular people rarely teleport—they follow links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Digital Paleozoic" (+60 points):&lt;/strong&gt; Suddenly, a guest on Windows XP pops up in the logs. In 2026! It’s like a gentleman in rusty knight’s armor showing up at a high-tech gala. Most likely, we’re looking at an old botnet using ancient libraries for requests. This is a major red flag, almost a conviction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Cloud Residency" (+100 points):&lt;/strong&gt; If an IP check shows the guest lives in an Amazon, Hetzner, or DigitalOcean data center—game over. Normal people don't browse the web while physically sitting in a server rack in Frankfurt. This is an instant bot status, with no right to appeal.&lt;/p&gt;

&lt;p&gt;When the total score in this "suspect card" breaks the 70-point threshold, the visit is officially flagged as "non-human."&lt;/p&gt;

&lt;p&gt;This approach allows us to stay polite to real people who might just have a weird browser config or a paranoid antivirus stripping referers, while ruthlessly filtering out the "stealth bots" trying to leak into your clean analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: Snitch Ports and the Referer Loop
&lt;/h2&gt;

&lt;p&gt;Sometimes bots mess up on things so absurd that it feels like the script developer decided to wink at me from the shadows. In version 1.3.0, I implemented two traps that have become my favorites in the hunt for automated "guests."&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Port Leak: The Spy Who Came from the Control Panel
&lt;/h3&gt;

&lt;p&gt;Imagine a customer walking into your store, but their badge says "Junior Hacking Intern at cPanel." You’d be on guard, right? That’s exactly how it looks in the logs.&lt;/p&gt;

&lt;p&gt;A real user comes to you from Google, social media, or types the address manually. But vulnerability scanners often work "in tandem" with hijacked hostings or control panels. As a result, a header hits my analytics: Referer:  &lt;code&gt;https://some-shadow-site.com:2083&lt;/code&gt;  ...&lt;/p&gt;

&lt;p&gt;Port  &lt;code&gt;:2083&lt;/code&gt;  is the classic entry point for cPanel. A real person cannot "accidentally" follow a link from another server's admin panel to your site. It’s a dead giveaway that someone just finished dissecting a neighboring hosting and is now targeting your project. In my config, these "snitch ports" (including Plesk  &lt;code&gt;:8443&lt;/code&gt;  and Webmin  &lt;code&gt;:10000&lt;/code&gt;  ) have a weight of 100.&lt;/p&gt;

&lt;p&gt;One hit like that, and the bot is instantly sent to the digital ban list before it can even say "Hello World."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Referer Loop:** Failing the Turing Test at the Starting Line
&lt;/h3&gt;

&lt;p&gt;Bots try their hardest to look like "one of us." They know that a missing referer is suspicious (&lt;em&gt;we already assigned +35 points for that in Chapter 1&lt;/em&gt;), so they try to simulate it. But sometimes they do it with the grace of a bull in a china shop.&lt;/p&gt;

&lt;p&gt;I call this the &lt;strong&gt;Referer Loop&lt;/strong&gt;. A bot lands on your page and sets the Referer header to... that exact same page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bot Logic:&lt;/strong&gt; "If I say I came from here, the server will think I just clicked an internal link."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Analytics Logic:&lt;/strong&gt; "Buddy, this is your first visit. You haven't been inside yet to navigate from anywhere to anywhere."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bot Logic:&lt;/strong&gt; "If I say I came from here, the server will think I just clicked an internal link."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Analytics Logic:&lt;/strong&gt; "Buddy, this is your first visit. You haven't been inside yet to navigate from anywhere to anywhere."&lt;br&gt;
If the system sees a page referencing itself during the very first appearance of that IP in a session—the masks are off. Turing test failed, and the bot score gets another 50 points. A living human must first arrive at the site before they can start moving between its pages.&lt;/p&gt;
&lt;h3&gt;
  
  
  How it looks in code
&lt;/h3&gt;

&lt;p&gt;For those who like to "get their hands on" the implementation, here is the config snippet responsible for this "interrogation" stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/** 
* 4. SUSPICIOUS REFERER PORTS 
* Technical ports in the Referer header (cPanel, Plesk, etc.) 
* Real users almost never arrive from these ports. 
*/&lt;/span&gt;
&lt;span class="s1"&gt;'port_leak'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;2082&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2083&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// cPanel&lt;/span&gt;
    &lt;span class="mi"&gt;2086&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2087&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// WHM&lt;/span&gt;
    &lt;span class="mi"&gt;8443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8880&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Plesk&lt;/span&gt;
    &lt;span class="mi"&gt;2222&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// DirectAdmin&lt;/span&gt;
    &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Webmin&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'weights'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Traffic arriving from technical control panels&lt;/span&gt;
    &lt;span class="s1"&gt;'port_leak'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Referer loop detection (URL == Referer on first hit)&lt;/span&gt;
    &lt;span class="s1"&gt;'referer_loop'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Chapter 3: Snowball Effect — The Magic of Retroactive Retribution
&lt;/h2&gt;

&lt;p&gt;If the previous methods were the "border patrol" at the gates, then the Snowball Effect is the work of internal security with access to the archives. This is the most powerful and, I admit, my favorite feature of the 1.3.0 release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imagine this:&lt;/strong&gt; a bot visits you. Not a clunky Python script, but an elite "stealth operative." For the first three pages, it behaves perfectly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It maintains pauses, mimicking human reading patterns.&lt;/li&gt;
&lt;li&gt;It provides flawless headers.&lt;/li&gt;
&lt;li&gt;It even "scrolls" (simulates activity).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My system looks at it and says: "Okay, buddy, you look human. Go ahead." We log it as a "clean" visitor, and it ends up in your beautiful statistics. But on the fourth page, the bot slips up. &lt;/p&gt;

&lt;p&gt;Professional curiosity takes over, and it wanders into a "honeypot"—trying to read the /.env file or peek into /wp-admin.&lt;/p&gt;

&lt;p&gt;In older versions of analytics, we would have simply flagged that fourth visit as "bot activity." But this is absurd! If it tried to steal your access keys at 2:05 PM, it means that at 2:00 PM it wasn't a fan reading your Laravel articles either. It was an enemy lying in wait.&lt;/p&gt;

&lt;p&gt;In version 1.3.0, the system switches to Retroactive Retribution mode: "You sneaky piece of hardware!" says the package. "If you're a bot now, you've been a bot all along."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works technically:&lt;/strong&gt; As soon as an IP address hits a critical weight (threshold) or lands in a Honeypot, the package triggers a background task (via Laravel Queue or Command). It pulls up the history of that IP for the last 60 days and "re-colors" all its past, seemingly "clean" visits with bot status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/** 
* Snowball Effect (Retroactive Cleanup) 
* Automatically flags historical sessions of a newly identified bot. 
*/&lt;/span&gt;
&lt;span class="s1"&gt;'cumulative'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'enabled'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'history_window_days'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// How far back we are willing to "take revenge"&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your statistics are cleaned up retroactively. You open the dashboard and see that the "garbage" hits that managed to slip through disguised as real people have simply vanished. This isn't just analytics; it's a self-cleaning ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 4: UA Detector or "Your Mustache is Falling Off"
&lt;/h2&gt;

&lt;p&gt;To understand how my config works, you need to look at what bots are actually sending in their headers. Here are two classic examples from my logs that are guaranteed to fail the check:&lt;/p&gt;

&lt;h3&gt;
  
  
  "The Humble Automator"
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;python-requests/2.31.0&lt;/code&gt; or &lt;code&gt;GuzzleHttp/7&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's a bot:&lt;/strong&gt; No guesswork needed here. These request libraries honestly admit what they are. In the config, they live in the  suspicious_ua  section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
* 2. SUSPICIOUS USER-AGENTS (Common fragments)
* Common library or tool strings used by scrapers and automated tools.
*/&lt;/span&gt;
&lt;span class="s1"&gt;'suspicious_ua'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s1"&gt;'python-requests'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'guzzlehttp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'go-http-client'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'curl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  "The Forgetful Mimic"
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's a bot:&lt;/strong&gt; Windows NT 6.1 (Windows 7) and IE 9 in 2026? This is either a very sad computer in a rural library or (more likely in 99% of cases) an old botnet using decade-old presets. This gets penalized via  obsolete_os  :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
* Obsolete OS versions (Windows XP, 2000, etc.) 
*/&lt;/span&gt;
&lt;span class="s1"&gt;'obsolete_os'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s1"&gt;'Windows NT 5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'Windows NT 6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'Mac OS X 10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code Implementation:&lt;/strong&gt; Setting the Weights&lt;br&gt;
And here is the heart of the system—the weight matrix in  &lt;code&gt;config/visit-analytics.php&lt;/code&gt;  . This is where we define how strict our "border control" will be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Scoring Weights (Suspicion Matrix) */&lt;/span&gt;
&lt;span class="s1"&gt;'weights'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Direct entry to internal page without Referer (bot behavior)&lt;/span&gt;
    &lt;span class="s1"&gt;'no_referer'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Attempting to trick the system by setting Referer to current URL&lt;/span&gt;
    &lt;span class="s1"&gt;'referer_loop'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// If IP belongs to a data center (AWS, Hetzner, etc.) — instant 100% bot&lt;/span&gt;
    &lt;span class="s1"&gt;'datacenter'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Click speed exceeding human capabilities (&amp;lt; 2 seconds between pages)&lt;/span&gt;
    &lt;span class="s1"&gt;'speed_anomaly'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Visiting a "honeypot" (e.g., /.env or /wp-admin)&lt;/span&gt;
    &lt;span class="s1"&gt;'honeypot'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Those weird ports in the referer (cPanel, Plesk)&lt;/span&gt;
    &lt;span class="s1"&gt;'port_leak'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Honeypot Works
&lt;/h3&gt;

&lt;p&gt;This is the most effective and fastest way to purge your logs. We add a list of paths to the config that a normal user of your Laravel app would never visit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'honeypot_paths'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'/.env'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'/wp-admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'/.git'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'/bitrix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'/config.php'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'/phpinfo.php'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment a script knocks on  &lt;code&gt;/.env&lt;/code&gt;  , it gets  &lt;code&gt;is_bot = true&lt;/code&gt;  and a maximum  &lt;code&gt;bot_score&lt;/code&gt;  . And thanks to the Snowball Effect, all its previous attempts to "act human" on the home page are instantly nullified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 5: What’s Next? (Filament Announcement)
&lt;/h2&gt;

&lt;p&gt;Collecting data and filtering it with virtuosity is only half the battle. The real "dopamine hit" comes when you see the results of your work not in raw database tables, but in beautiful, intuitive graphics. There is nothing more satisfying than watching the gray "bot curve" decline while the green graph of real, live humans grows steadily.&lt;/p&gt;

&lt;p&gt;Right now, I’m working on bringing all this "looking-glass magic" into the visual plane. In the next major release, I’m planning full integration with Filament. Why dig through configs via the terminal when you can manage your digital security through a stylish UI?&lt;/p&gt;

&lt;h3&gt;
  
  
  Here’s what’s on the horizon:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Real-time Dashboard:&lt;/strong&gt; A set of widgets showing your site’s "pulse" in real time. You’ll literally see who the system has "neutralized" just now and for what reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive Bot Map:&lt;/strong&gt; Visualization of attacks and visits. &lt;br&gt;
You’ll be able to see clearly where the Palo Alto scanners are coming from and where the "honest" search engines are lurking. It will look like a cyber-command headquarters.&lt;/p&gt;

&lt;p&gt;Admin Panel Weight Management: No more editing &lt;code&gt;.php&lt;/code&gt; files just to change a single sensitivity threshold. You’ll be able to fine-tune scoring weights, add new suspicious ports, or update your honeypot list with a few clicks without leaving the comfort of the Filament panel.&lt;/p&gt;

&lt;p&gt;But first things first. The path to perfect analytics is a marathon, not a sprint. So stay tuned: I promise the updates will be "tasty," technically elegant, and incredibly useful for those who value the purity of their data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Epilogue: Purity as a Religion
&lt;/h2&gt;

&lt;p&gt;Ultimately, after implementing behavioral analysis and port checks, my traffic charts have "slimmed down" significantly. Но это та самая диета, которая идет на пользу. Now I know for sure: if I see a visit from Linux via &lt;code&gt;IPv6&lt;/code&gt;, it's either my old friend, the "digital detective," or a genuinely interested pro—not just another Palo Alto Networks script that decided to read my Terms of Service.&lt;/p&gt;

&lt;p&gt;Logs are not just text. They are a signature. And now, my Laravel package can distinguish the calligraphy of a living person from the mechanical stamps of a typewriter.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>analytics</category>
      <category>security</category>
    </item>
    <item>
      <title>Through the Looking Glass of Logs: Karachi Police, DuckDuckGo, and IPv6 Magic</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Fri, 29 May 2026 19:33:34 +0000</pubDate>
      <link>https://dev.to/alantalex/through-the-looking-glass-of-logs-karachi-police-duckduckgo-and-ipv6-magic-7e2</link>
      <guid>https://dev.to/alantalex/through-the-looking-glass-of-logs-karachi-police-duckduckgo-and-ipv6-magic-7e2</guid>
      <description>&lt;p&gt;Remember when I said reality turned out to be harsher? When you open the logs of a custom-built analytics package after a couple of days, you expect to see hits from a few friends and, in the best-case scenario, your mom. Well, maybe a lost translator bot.&lt;/p&gt;

&lt;p&gt;Instead, I discovered that the server had become a point of interest for folks who don't just "scroll the feed," but dissect the internet down to its molecules. My little project suddenly transformed into a testing ground for those who prefer silence and &lt;code&gt;IPV6&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read the first part of the story about how it all began in the article: &lt;br&gt;
&lt;a href="https://oleant.dev/en/blog/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production" rel="noopener noreferrer"&gt;How I Built My Own Laravel Analytics Package (and Almost Didn't Crash Production)&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this series, we’ll step "through the looking glass": we’ll find out who &lt;strong&gt;Palo Alto Networks&lt;/strong&gt; are, why bots read my Terms of Service in 7 seconds, and why &lt;em&gt;Linux + DuckDuckGo&lt;/em&gt; is now the official mark of quality for a visitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Chapter 1: Ghost in the Shell (Linux) and the Guest from the Future
&lt;/h2&gt;

&lt;p&gt;The first one to make my freshly-baked analytics package "rustle" like a pro was a guest whose profile looked like a canonical portrait of a cyber-detective. Imagine: Sunday, late evening. Most people are scrolling social media feeds on iPhones. But in my logs, a character pops up entering via &lt;code&gt;IPv6&lt;/code&gt;, using &lt;strong&gt;Linux&lt;/strong&gt;, and introducing themselves as &lt;strong&gt;Firefox 140.0&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For a second, I froze. It’s April 2026; stable browser versions have barely crawled into the 130s. Who is this? A time traveler? A Mozilla nightly build tester? Or simply someone who knows a little more than "everything" about anonymity and header spoofing? In the world of info-sec, this is a clear signal: we are looking at a "power user" who consciously uses a non-standard stack to avoid blending into the crowd.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He came from the "Looking Glass" — via &lt;strong&gt;DuckDuckGo&lt;/strong&gt;. Its users hate being tracked and know how to look where others are forbidden to enter. And this guest didn't come to my audit just to "click buttons." He behaved like a professional inspector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First — Politeness.&lt;/strong&gt; Checked &lt;code&gt;robots.txt&lt;/code&gt;. Knocked before entering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jurisdictional Study.&lt;/strong&gt; In a matter of seconds, he "swallowed" the Terms of Service page. Pros always check whose territory they are on before pulling the trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Action.&lt;/strong&gt; He wasn't interested in articles about choosing a framework. He came for a specific tool — a &lt;em&gt;deep DNS audit&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This visit is the highest form of recognition. When a person using software from the future (Firefox 140!) and hiding behind IPv6 tunnels chooses your tool for work — it means you’ve made something truly worthwhile. But it also means your server must be ready to receive such "ghosts." If your security is leaky, such a pro will see it before the first pixel of the interface even renders.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Chapter 2: DNS Detective. The "Karachi Police" Case
&lt;/h2&gt;

&lt;p&gt;While my analytics package imperturbably digested traffic, the "Linux user" from the Netherlands decided that enough was enough with the studying of terms — it was time to move on to "field tests." And as his target, he chose not some local spare parts shop, but the domain &lt;code&gt;karachipolice.gov.pk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the official website of the police of Pakistan’s largest city pops up in the Sunday audit logs, you involuntarily adjust your imaginary detective hat. What was it? Subtle trolling, a real investigation, or did someone on the other side of the ocean just decide to test their professional aptitude via an independent tool?&lt;/p&gt;

&lt;p&gt;Either way, the "Run Audit" button was pressed. And &lt;strong&gt;Oleant&lt;/strong&gt; laid out evidence on the table that would make any sysadmin in Karachi's eye twitch:&lt;/p&gt;

&lt;h3&gt;
  
  
  DMARC: null
&lt;/h3&gt;

&lt;p&gt;This was the first and biggest "red flag." DMARC is, essentially, passport control for your mail. If it equals &lt;code&gt;null&lt;/code&gt;, it means the domain has no policy protecting against email spoofing. Any freshman hacker could send an email with the subject "Urgent Fine" on behalf of the Chief of Police.&lt;/p&gt;

&lt;h3&gt;
  
  
  SPF with a "soft" character
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;~all&lt;/code&gt; policy in an SPF record is like a lock that is closed, but if you pull hard enough, it opens. We’re telling the world which servers have the right to send mail, but then immediately adding: "But if it comes from someone else, accept it anyway, just mark it as suspicious."&lt;/p&gt;

&lt;h3&gt;
  
  
  IPv6 Facade and Hosted Email
&lt;/h3&gt;

&lt;p&gt;The Karachi Police website honestly resolves via modern &lt;strong&gt;AAAA records&lt;/strong&gt; (hello, IPv6 future!), but their entire email infrastructure hangs on standard servers of a popular low-cost host. It’s like putting an old carburetors from a vintage Lada into a modern supercar.&lt;/p&gt;

&lt;p&gt;Observing this audit through the prism of my analytics package, I realized one important thing: there are no borders on the web. A person sitting under the protection of a European provider via an IPv6 tunnel exposes critical security gaps in an Asian government agency in seconds, using a tool written in free time by a web developer for web developers somewhere in Europe.&lt;/p&gt;

&lt;p&gt;This is the "Looking Glass," where security isn't the thickness of the walls, but the correctness of a single line in a DNS record. If your domain hasn't been audited in a while, don't hesitate to visit Oleant and run a &lt;a href="https://oleant.net/security-tools/dns-records-check" rel="noopener noreferrer"&gt;free DNS audit&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Chapter 3: Dancing with Shadows
&lt;/h2&gt;

&lt;p&gt;If in the first chapter we met a live pro, in the third, the "corporate ghosts" enter the game. When you launch a project, you feel like it's just you and a blank sheet of code. But as soon as you record the first hits from &lt;strong&gt;Palo Alto Networks&lt;/strong&gt; bots, you realize: the project is already being watched.&lt;/p&gt;

&lt;p&gt;Don't be scared; they haven't hacked my logs. In the world of "heavy duty" security, everything works more elegantly. Palo Alto and their peers build global threat maps by analyzing traffic worldwide. How do they "see" this?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 01 | Network Footprint:&lt;/strong&gt; When my server queries DNS records or ports of a domain, for external monitoring systems, my domain suddenly starts behaving like an active scanner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 02 | Reputation Scoring:&lt;/strong&gt; If your domain regularly "communicates" with government or corporate nodes in audit mode, Palo Alto's algorithms flag it as a technical analysis node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 03 | Verdict:&lt;/strong&gt; The domain is assigned a category. Being in "InfoSec" is much more prestigious and safer for reach than hanging in "Uncategorized" or "Suspicious."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why is this important for us? Because in 2026, domain reputation is your passport to the "clean internet." If your project behaves professionally, uses a fresh stack (&lt;em&gt;Laravel 12!&lt;/em&gt;), and is correctly configured, even giants like Palo Alto begin to treat your traffic as a legitimate research tool.&lt;/p&gt;




&lt;h3&gt;
  
  
  Epilogue
&lt;/h3&gt;

&lt;p&gt;Security isn't scary. It's hygiene. It's the ability to understand who is knocking on your door and why. My analytics package showed me this clearly: from live Linux enthusiasts to the soulless but damn smart machines of Palo Alto.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Logs don't lie. They tell stories cooler than any spy novel. The main thing is knowing how to read them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And in the next episode...&lt;/strong&gt;&lt;br&gt;
We’ll break down how one forgotten comma in a Content Security Policy (CSP) can turn your modern project into a pumpkin for Googlebot and why "Mixed Content" is a death sentence for your SEO.&lt;/p&gt;

&lt;p&gt;You can read the article about auditing security headers here: &lt;a href="https://oleant.dev/en/blog/your-digital-fortress-why-a-security-headers-audit-is-essential" rel="noopener noreferrer"&gt;Your Digital Fortress: Why a Security Headers Audit is Essential&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TAKE CARE OF YOUR HTTP HEADERS.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;SEE YOU THROUGH THE LOOKING GLASS.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>networking</category>
    </item>
    <item>
      <title>How I Built My Own Laravel Analytics Package (and Almost Didn't Crash Production)</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Thu, 21 May 2026 21:56:48 +0000</pubDate>
      <link>https://dev.to/alantalex/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production-l75</link>
      <guid>https://dev.to/alantalex/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production-l75</guid>
      <description>&lt;h2&gt;
  
  
  Why Not Google Analytics? (Or why I love reinventing the wheel)
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;h3&gt;
  
  
  Remember the General Data Protection Regulation (GDPR)?
&lt;/h3&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;(To learn how to avoid legal trouble with data protection laws, check out the article &lt;a href="https://oleant.dev/en/blog/gdpr-without-the-headache-a-guide-for-web-developers-in-germany" rel="noopener noreferrer"&gt;GDPR Without the Headache: A Guide for Web Developers in Germany &lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first candidate was Matomo (formerly Piwik):&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The second tool I looked at was Plausible / Fathom:&lt;/em&gt; 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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 &lt;a href="https://oleant.net" rel="noopener noreferrer"&gt;oleant.net&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;For those interested in diving into the code, the package is called &lt;strong&gt;&lt;code&gt;oleant/laravel-visit-analytics&lt;/code&gt;&lt;/strong&gt;, compatible with Laravel versions 10/11/12. GitHub link: &lt;a href="https://github.com/Oleant-NET/laravel-visit-analytics" rel="noopener noreferrer"&gt;https://github.com/Oleant-NET/laravel-visit-analytics&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Heart of the System: Middleware and the Magic of the Aftertaste
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That’s why a crucial decision was made — to use the &lt;strong&gt;&lt;code&gt;terminate()&lt;/code&gt;&lt;/strong&gt; method. In a Laravel Middleware, it’s like a polite waiter: he brings you the check and smiles (the &lt;strong&gt;&lt;code&gt;handle()&lt;/code&gt;&lt;/strong&gt; method), and only after you’ve already left the restaurant does he go back to wipe the table and log the tip (the &lt;strong&gt;&lt;code&gt;terminate()&lt;/code&gt;&lt;/strong&gt; method).&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Logic to exclude non-target clients, like Admin etc.&lt;/span&gt;
        &lt;span class="c1"&gt;// …&lt;/span&gt;
        &lt;span class="c1"&gt;// Next, write our visitors to the database&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;logVisit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// If the DB takes a nap, we won't wake the user with a 500 error&lt;/span&gt;
        &lt;span class="nc"&gt;\Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Analytics failed, but we're keeping our cool: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;Lifehack&lt;/strong&gt;: 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.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GDPR on the Fly&lt;/strong&gt;: How Not to Become Public Enemy No. 1&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back to IP anonymization:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Before: 192.168.1.154 -&amp;gt; After: 192.168.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload:&lt;/strong&gt; Gathering Only the Goodies&lt;br&gt;
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 &lt;strong&gt;&lt;code&gt;array_intersect_key&lt;/code&gt;&lt;/strong&gt;. Now, only what I’ve personally authorized in the config ends up in the logs. Clean, orderly, and zero fluff.&lt;/p&gt;

&lt;p&gt;The default set in the package config looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'whitelist'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'utm_source'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'utm_medium'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'utm_campaign'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'utm_term'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'utm_content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'ref'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But you can, of course, change it by publishing the config in your project first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bash&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"visit-analytics-config"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>privacy</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I don’t trust my own deployments (and why you should audit your Security Headers)</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Sat, 11 Apr 2026 07:15:18 +0000</pubDate>
      <link>https://dev.to/alantalex/why-i-dont-trust-my-own-deployments-and-why-you-should-audit-your-security-headers-19cm</link>
      <guid>https://dev.to/alantalex/why-i-dont-trust-my-own-deployments-and-why-you-should-audit-your-security-headers-19cm</guid>
      <description>&lt;p&gt;As a Laravel developer, I’ve always felt pretty safe. Modern frameworks do a lot of heavy lifting, but here’s the cold truth: even the most secure backend can be undermined by a "leaky" frontend or a misconfigured Nginx.&lt;/p&gt;

&lt;p&gt;I caught myself constantly jumping between third-party tools every time I deployed a new feature just to make sure I hadn't messed up my Strict-Transport-Security or broken my Content-Security-Policy. Eventually, I got tired of the routine and built my own module within Oleant.&lt;/p&gt;

&lt;p&gt;What’s the deal?&lt;br&gt;
I’m talking about the Security Headers Audit. It’s not just another tool that says "everything is bad"; it breaks down exactly what's happening under the hood of your URL.&lt;/p&gt;

&lt;p&gt;Why it matters (The Tech Side):&lt;br&gt;
A lot of devs think SSL/TLS is the finish line. But without the right headers, you're still vulnerable to:&lt;/p&gt;

&lt;p&gt;Clickjacking (lack of X-Frame-Options).&lt;/p&gt;

&lt;p&gt;MIME-sniffing (no X-Content-Type-Options).&lt;/p&gt;

&lt;p&gt;XSS attacks that a solid CSP could have neutralized instantly.&lt;/p&gt;

&lt;p&gt;My Implementation:&lt;br&gt;
I built this using Laravel 11 + Inertia.js + Vue 3. This stack allowed me to make the audit process incredibly snappy. You drop the URL, and the Vue component reactively renders the status of every critical header.&lt;/p&gt;

&lt;p&gt;Give it a spin:&lt;br&gt;
I’ve exposed this tool as a dedicated route here:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://oleant.net/security-tools/headers-audit" rel="noopener noreferrer"&gt;https://oleant.net/security-tools/headers-audit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s not a bloated "all-in-one" suite — it’s a precision scalpel. If you’re deploying something today, just throw your link in there and see how much "red" pops up. I actually found a few embarrassing gaps in my own older projects this way.&lt;/p&gt;

&lt;p&gt;Follow my journey: &lt;a href="https://oleant.dev/en/blog" rel="noopener noreferrer"&gt;https://oleant.dev/en/blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>laravel</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop guessing, start auditing: Why I built a custom Web Performance tool for Laravel devs</title>
      <dc:creator>Oleksii Antoniuk</dc:creator>
      <pubDate>Tue, 07 Apr 2026 06:57:31 +0000</pubDate>
      <link>https://dev.to/alantalex/stop-guessing-start-auditing-why-i-built-a-custom-web-performance-tool-for-laravel-devs-5h6o</link>
      <guid>https://dev.to/alantalex/stop-guessing-start-auditing-why-i-built-a-custom-web-performance-tool-for-laravel-devs-5h6o</guid>
      <description>&lt;p&gt;Hi DEV community! 👋 &lt;/p&gt;

&lt;p&gt;As a Senior Laravel Developer, I've always been obsessed with one thing: &lt;strong&gt;Performance.&lt;/strong&gt; We all use Lighthouse and PageSpeed Insights, but I felt something was missing—a tool that speaks the language of developers and gives actionable SEO insights without the fluff.&lt;/p&gt;

&lt;p&gt;That's why I started building &lt;a href="https://oleant.net" rel="noopener noreferrer"&gt;oleant.net&lt;/a&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  What’s the goal?
&lt;/h3&gt;

&lt;p&gt;My mission is to simplify &lt;strong&gt;Core Web Vitals&lt;/strong&gt; optimization. It's not just about the score; it's about the user experience and how search engines perceive your architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'm sharing here:
&lt;/h3&gt;

&lt;p&gt;On this profile and my technical blog at &lt;a href="https://oleant.dev" rel="noopener noreferrer"&gt;oleant.dev&lt;/a&gt;, I’ll be deep-diving into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced Laravel optimization techniques.&lt;/li&gt;
&lt;li&gt;Real-world Core Web Vitals case studies.&lt;/li&gt;
&lt;li&gt;Building high-performance SEO tools from scratch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear your thoughts! What's your biggest struggle when it comes to web performance?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the auditor here:&lt;/strong&gt; &lt;a href="https://oleant.net" rel="noopener noreferrer"&gt;oleant.net&lt;/a&gt; 🚀&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>performance</category>
      <category>webdev</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
