Search by

bahdan / symfony-privacy-analytics-bundle

bhal

Zero-cookie, GDPR-compliant server-side privacy analytics bundle for Symfony

Package info

github.com/bahdanhal/symfony-privacy-analytics-bundle

Type:symfony-bundle

pkg:composer/bahdan/symfony-privacy-analytics-bundle

Statistics

Installs: 326

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.6.1 2026-11-13 21:15 UTC

This package is auto-updated.

Last update: 2026-11-13 21:16:28 UTC


README

A lightweight, zero-cookie, GDPR-compliant server-side web analytics bundle for Symfony applications.

Packagist · GitHub

Used in production by Stackhal, IleZa.pl and bahdanhal.pl.

Key Features

  • Zero Cookies / No Consent Banner Needed: Anonymously hashes IP + User-Agent + secret with HMAC-SHA256.
  • Bot & Crawler Filtering: Ignores automated crawlers, spiders, and testing clients automatically.
  • Privacy Header Compliance: Honors DNT (Do Not Track) and Sec-GPC (Global Privacy Control) request headers.
  • Referrer Categorization: Automatically groups incoming traffic into search, social, referral, internal, and direct. A visit without a referrer is attributed to the hostname in utm_source when there is one (for example utm_source=chatgpt.com); other utm_source values and the rest of the query string are ignored.
  • Dual Storage: Supports Doctrine ORM (PostgreSQL/SQLite/MySQL) and monthly partitioned JSONL storage with streaming retention pruning.
  • SQL Aggregation: Doctrine summaries aggregate counts, unique visitors, sources, referrers, paths, and daily metrics in the database rather than hydrating raw page views.
  • Cached Summaries: Dashboard aggregates use the application's PSR-6-compatible Symfony cache for a configurable short TTL.
  • Optional Async Ingestion: Symfony Messenger can move writes off the request worker for traffic spikes.
  • Extensible Bot Filtering: Add project-specific user-agent substrings without forking the bundle.

Installation

composer require bahdan/symfony-privacy-analytics-bundle

Doctrine is optional. Install it only when selecting the Doctrine storage backend:

composer require doctrine/orm doctrine/dbal

The JSONL backend does not register or resolve Doctrine services. Existing page-views.jsonl files remain readable while new events are written to monthly page-views-YYYY-MM.jsonl partitions.

Doctrine entity mappings are registered automatically. Your application still owns schema migrations; generate and review a migration after enabling Doctrine storage.

Configuration

Register the bundle in config/bundles.php:

return [
    // ...
    Bahdan\PrivacyAnalyticsBundle\PrivacyAnalyticsBundle::class => ['all' => true],
];

Configure the storage and privacy secret:

# config/packages/privacy_analytics.yaml
privacy_analytics:
  secret: '%kernel.secret%'
  storage: doctrine
  retention_days: 90
  summary_cache_ttl: 60
  custom_bot_patterns:
    - your-internal-monitor

Browser beacon

For client-side page views, configure privacy_analytics.mode: beacon and install the bundle assets during deployment:

php bin/console assets:install public

Replace any existing inline analytics beacon with the bundle script in your base template:

<script src="/bundles/privacyanalytics/beacon.js" defer></script>

Alternatively, serve the script through Symfony by importing its controller:

privacy_analytics_script:
  resource: '@PrivacyAnalyticsBundle/src/Presentation/Http/BeaconScriptController.php'
  type: attribute

For bundles resolved at the package root, the resource above is relative to that root. Include /api/pa/beacon.js in the script tag when using this route. This also supports deployments with a separate static-file image.

Set data-beacon-path="/analytics/hit" if your beacon route uses a custom path. The route must match the configured privacy_analytics.beacon_path.

The script skips browsers reporting navigator.webdriver, honors Do Not Track and Global Privacy Control, and records each page once after it becomes visible. Prerendered and background pages wait until activation or visibility. Automated browsers can conceal WebDriver, so recorded page views do not establish human identity. No interaction requirement or persistent visitor identifier is added.

High-traffic ingestion with Messenger

Install Messenger, enable async ingestion, and route the bundle message to your asynchronous transport:

composer require symfony/messenger
# config/packages/privacy_analytics.yaml
privacy_analytics:
  async: true

# config/packages/messenger.yaml
framework:
  messenger:
    transports:
      analytics: '%env(MESSENGER_TRANSPORT_DSN)%'
    routing:
      Bahdan\PrivacyAnalyticsBundle\Message\RecordPageView: analytics

Run a normal Messenger worker for the analytics transport. With Doctrine storage, dashboard summaries remain database-side aggregate queries. JSONL summaries stream only partitions that can overlap the requested period; Doctrine is recommended for sustained high traffic.

Dashboard data

Inject Bahdan\PrivacyAnalyticsBundle\Application\TrafficAnalytics into a controller and return summary() as JSON or render it in Twig. The stable result contains seven-day and thirty-day totals, sources, referring domains, top paths, and a 30-day daily series, so applications can use Chart.js or their existing design system without the bundle imposing frontend dependencies.

#[Route('/admin/analytics', methods: ['GET'])]
public function analytics(TrafficAnalytics $analytics): JsonResponse
{
    return $this->json($analytics->summary(new \DateTimeImmutable('now', new \DateTimeZone('UTC'))));
}

License

MIT